﻿// Ground.cpp : 实现文件
//

#include "stdafx.h"
#include "GHCY.h"
#include "../Hui/Hui.h"
#include "../HComm/HRecordset.h"
//#include "Direct.h"
#include "Car2.h"
#include "LiftPoint.h"
#include "ProductPoint.h"
#include "SpecifyPoint.h"
#include "StorePoint.h"
#include "RechargePoint.h"
#include "StartPoint.h"
#include "MiddlePoint.h"
#include "WaitPoint.h"
#include "PointSocket.h"
#include "GException.h"
#include "LogList.h"
//#include "PointDemo.h"
#include "DlgCallPoint.h"
#include "AGVComm.h"
#include "Collection.h"
#include "Ground.h"
#include "NodeColl.h"
#include "Floors.h"

// Ground
/***********************
场地类，从数据库中读取场地，画场地。
************************/

#ifdef _DEBUG
	#define new DEBUG_NEW
#endif

// Driect2D支持
#pragma comment(lib,"D2d1.lib")

IMPLEMENT_DYNAMIC(Ground, HCtrl)

BEGIN_MESSAGE_MAP(Ground, HCtrl)
	//ON_NOTIFY(HNM_QUESTRUN, 0, onQuestRun)
	ON_COMMAND(TID_PAUSE, onPauseAll)
	ON_COMMAND(TID_FILLALL, onFillAll)
	ON_COMMAND(TID_CLEARALL, onClearAll)
	ON_COMMAND_RANGE(TID_DISABLECAR, TID_DISABLECAR + MAXAGV - 1, onDisableCar)	// 设置Car参数
	ON_COMMAND_RANGE(TID_PAUSECAR, TID_PAUSECAR + MAXAGV - 1, onPauseCar)	// 暂停Car
END_MESSAGE_MAP()

CArray<RoadNode, RoadNode&>* afxpArrRoads = 0;

//回调函数具体实现 https://blog.csdn.net/fairytailq/article/details/78376414#:~:text=MFC%E4%B8%AD%E5%BA%94%E8%AF%A5%E6%9C%89%E4%B8%A4%E7%B1%BB%E5%9B%9E
void Ground::CallBackWcs(PBYTE pRecBuf, int len)
{
	ASSERT(theApp.m_pGround);
	theApp.m_pGround->wcsRead(pRecBuf, len);
}

Ground::Ground(HCtrl * pParent, LPCRECT prc)
	: HCtrl(pParent, prc)
	//, m_nWidth(0)
	//, m_nHeight(0)
	, m_nPadding(50)	// 默认留30象素边
	//, m_nDirction(0)
	//, m_fSpeed(0)
	, m_socketWcs(L"", 10000)				// 对方端口是10000
	, m_fWheelScale(1)
	, m_fWheelX(0)
	, m_fWheelY(0)
	//, m_pAStar(0)
	, m_pBrushGround(0)
	, m_bDrawBackground(FALSE)
{
	theApp.m_pGround = this;	// 尽量往前放，后面好用。

	m_socketWcs.pCallBack = CallBackWcs;

	// 背景颜色，钛金
	//setBackColor( RGB(255, 55, 40) );	// 20140813 改到上级控件设背景
	//setBorderColor( 0x555555 );

	// 字体
	setFont( 16, 0, L"宋体" );
	setDock( HDOCK_FULLHORIZONTAL | HDOCK_FULLVERTICAL );
	setTextColor( 0x00dddddd );

	//m_arrRunCars.SetSize( 0, 32 );	// 预留一定空间

	//m_arrRecharges.SetSize( 0, 50 );		// 预留一定空间
	//m_arrProducts.SetSize( 0, 50 );		// 预留一定空间
	//m_arrWarehouse.SetSize( 0, 50 );		// 预留一定空间
	m_arrEndPoint.SetSize( 0, 100 );		// 预留一定空间
	// 读楼层信息，放在loadCars后面
	afxFloors.loadFloors();
	// 读场地数据
	loadGround();
	// 读数据库道路数据
	loadRoads();
	//读转弯前行表
	loadTurnDis();
	// 读货托类型表，要在端点表之前
	theApp.m_tray.load();
	// 读端点数据，要在托类型表之后
	loadEndPoint();
	// 连接端点PLC，要求在loadEndPoint之后
	getPointPLC();
	// 读AGV数据
	loadCars();
	// 设置小圆圈几何体	// ?? 不适合潜伏AGV
	theApp.setGeo0();
	// 初始化路径算法
	//m_pAStar = new AStar( m_arrRoadNodes );
	// 装入起始点，其中用到m_nAStar
	// 20210413 改到CollStarts
	//loadStartPoint();

	// 神经网络
	m_colls.initNeural();

	// ?? 如果没有控制，光华测试线路
	if(!m_arrPointSocket.GetCount())
		theApp.m_bPlcCtrl = FALSE;		// 光华测试线路，没有呼叫柜

}

Ground::~Ground()
{
	//if( m_pAStar )
	//	delete m_pAStar;

	INT_PTR n = m_arrPointSocket.GetCount();
	for(INT_PTR i = 0; i < n; i++ )
	{
		delete m_arrPointSocket[i];
	}
}

// 读道路数据
void Ground::loadRoads()
{
	// 道路节点表
	//CString roadtable;
	//theApp.getSys( L"nodetable", roadtable );
	//if( roadtable.IsEmpty() )
	//	THROW( new HException( L"无法获取道路节点表" ) );

	CString strsql = L"SELECT \
`id`,\
`road`,\
`serial`,\
`x`,\
`y`,\
\
`cardID`,\
`length`,\
`angle`,\
`note`,\
`reverse`,\
\
`pointID`,\
`direct`,\
`dampUp`,\
`dampDown`, \
`floor`, \
\
`forwardSpeedUp`,\
`forwardSpeedDown`,\
`backwardSpeedUp`,\
`backwardSpeedDown` \
\
FROM `roads` \
ORDER BY `id`;";

	HRecordset rs( theApp.getDatabase() );
	rs.Open2( strsql );

	m_arrRoadNodes.SetSize( 0, 400 );	// 预留100个位置，避免内存碎片

	// 取字段名
	//CODBCFieldInfo fi;
	//rs.GetODBCFieldInfo( (short)5, fi );

	RoadNode rn;
	//int road = -1;

	//int nMaxCard = 0;

	CDBVariant var;	// var 要释放
	for( int i = 0; !rs.IsEOF(); i++)
	{
		// 读数据
		rs.GetFieldValue( (short)0, var );	// id
		rn.m_nID = var.m_lVal;

		rs.GetFieldValue( (short)1, var );
		rn.m_nRoad = var.m_lVal;
		//if( rn.m_nRoad <= 0 || rn.m_nRoad >= 3000 )	// 20180325 取消这个限制 // ???? 改东鹏程序
		//	THROW( new HException( L"路号必须大于零，小于3000" ) );

		rs.GetFieldValue( (short)2, var );
		rn.m_nSerial = var.m_lVal;

		rs.GetFieldValue( (short)3, var );
		rn.m_nX = var.m_lVal;		// 数据库中是毫米，转换成米，数字减小，小占画图资源
		//if( rn.m_nX > m_nWidth )
		//	m_nWidth = rn.m_nX;

		rs.GetFieldValue( (short)4, var );
		rn.m_nY = var.m_lVal;		// 数据库中是毫米，转换成米，数字减小，小占画图资源
		//if( rn.m_nY > m_nHeight )
		//	m_nHeight = rn.m_nY;

		rs.GetFieldValue( (short)5, var );
		rn.m_nCard = var.m_lVal ? var.m_lVal : -rn.m_nID;	//端点加上编号（m_nCard = -ID），便于导航。
		if (rn.m_nCard > m_nMaxCard)
			m_nMaxCard = rn.m_nCard;

		rs.GetFieldValue( (short)6, var );
		rn.m_nLength = var.m_lVal;							// 本段长度，mm

		rs.GetFieldValue( (short)7, var );
		rn.m_nAgvAngle = var.m_lVal;						// 本段角度，度

		if (var.m_lVal != 10000								// 10000表示下一段路是拉行
			&& (var.m_lVal < -180 || var.m_lVal > 180))		// 实际角度
		{
			CString str;
			str.Format(L"AGV角度（%i）无效", var.m_lVal);
			THROW(new HException(str));
		}


		//rs.GetFieldValue( (short)9, var );
		//rn.m_nReverse = var.m_dwType == DBVT_NULL ? 0 : var.m_lVal;	// 反向长度	// 20190215 这个字段不用了，用过卡行驶距离表取代
		
		rs.GetFieldValue( (short)10, var );
		rn.m_nPointID = var.m_lVal;				// 端点表索引		
		
		//rs.GetFieldValue( (short)11, var );
		//rn.m_nDirect = var.m_boolVal;			// 端点产品方向，0左向，1右向
		
		rs.GetFieldValue( (short)12, var );
		rn.m_nDampUp = var.m_lVal;				// 上行阻尼

		rs.GetFieldValue((short)13, var);
		rn.m_nDampDown = var.m_lVal;			// 下行阻尼

		rs.GetFieldValue((short)14, var);
		rn.m_nFloor = var.m_lVal;				// 楼层，从1开始


		rs.GetFieldValue((short)15, var);
		rn.m_bufSpeed[0] = var.m_lVal;			// 上行前进速度，推行，货叉向前

		rs.GetFieldValue((short)16, var);
		rn.m_bufSpeed[1]= var.m_lVal;			// 下行前进速度，推行，货叉向前

		rs.GetFieldValue((short)17, var);
		rn.m_bufSpeed[2]= var.m_lVal;			// 拉行，上行后退速度

		rs.GetFieldValue((short)18, var);
		rn.m_bufSpeed[3]= var.m_lVal;			// 拉行，下行后退速度

		m_arrRoadNodes.Add( rn );

		rs.MoveNext();

		// 求道路角度，A*中计算转弯加分时用
		if (rn.m_nSerial == 1)									// 第一点不计算
			continue;

		INT_PTR k = (INT_PTR)m_arrRoadNodes.GetCount() - 2;		// 上一点序号
		RoadNode& rn0 = m_arrRoadNodes[k];
		RoadNode& rn1 = m_arrRoadNodes[k+1];
		if (rn.m_nSerial == 2)									// 本路号的第二点
		{
			ASSERT(k >= 0);
			//计算路径角度
			float dx = (float)rn.m_nX - rn0.m_nX;
			float dy = (float)rn.m_nY - rn0.m_nY;
			rn1.m_fRoadRad = atan2(dy, dx);
			rn0.m_fRoadRad = rn1.m_fRoadRad;					// 设置本路号的第一点
		}
		else //if (rn.m_nSerial > 2)
		{
			ASSERT(k >= 1);
			rn1.m_fRoadRad = rn0.m_fRoadRad;					// 第三点以后取本路号的
		}
	}
	var.Clear();	// var 要释放
	rs.Close();		// 3ms 读数据库过程

	if( !m_arrRoadNodes.GetCount())
	{
		CString str;
		str.Format( L"没有道路数据" );
		THROW( new HException( str ) );
	}

	afxpArrRoads = &m_arrRoadNodes;

	// 20220705 初始化节点表，卡号相同的点合到一起
	//m_nodesColl.init(nMaxCard);

//#ifdef _DEBUG	// 调试状态，显示Open和Close两个表
//	TRACE( "\n" );
//	for( int q = 0, w = m_arrRoadNodes.GetCount(); q < w; q++ )
//	{
//		TRACE( " %i %i,", m_arrRoadNodes[q].m_nCard, m_arrRoadNodes[q].m_nReverse );
//	}
//	TRACE( "\n" );
//#endif
}

// 读场地数据
void Ground::loadGround()
{
	CString strsql = L"SELECT"
		L" `id`,"
		L" `road`,"
		L" `serial`,"
		L" `x`,"
		L" `y`,"
		L" `floor`"
		L" FROM `ground`"
		L" ORDER BY `id`;";

	HRecordset rs(theApp.getDatabase());
	rs.Open2(strsql);

	m_arrGround.SetSize(0, 300);	// 预留100个位置，避免内存碎片

	// 取字段名
	//CODBCFieldInfo fi;
	//rs.GetODBCFieldInfo( (short)5, fi );

	RoadNode rn;
	//int road = -1;

	CDBVariant var;	// var 要释放
	for (int i = 0; !rs.IsEOF(); i++)
	{
		// 读数据
		rs.GetFieldValue((short)0, var);	// id
		rn.m_nID = var.m_lVal;

		rs.GetFieldValue((short)1, var);
		rn.m_nRoad = var.m_lVal;

		rs.GetFieldValue((short)2, var);
		rn.m_nSerial = var.m_lVal;

		rs.GetFieldValue((short)3, var);
		rn.m_nX = var.m_lVal;		// 数据库中是毫米，转换成米，数字减小，小占画图资源
									//if( rn.m_nX > m_nWidth )
									//	m_nWidth = rn.m_nX;

		rs.GetFieldValue((short)4, var);
		rn.m_nY = var.m_lVal;		// 数据库中是毫米，转换成米，数字减小，小占画图资源
		//if( rn.m_nY > m_nHeight )
		//	m_nHeight = rn.m_nY;

		rs.GetFieldValue((short)5, var);
		rn.m_nFloor = var.m_lVal;		// 数据库中是毫米，转换成米，数字减小，小占画图资源

		m_arrGround.Add(rn);

		rs.MoveNext();
	}
	var.Clear();	// var 要释放
	rs.Close();

	//#ifdef _DEBUG	// 调试状态，显示Open和Close两个表
	//	TRACE( "\n" );
	//	for( int q = 0, w = m_arrRoadNodes.GetCount(); q < w; q++ )
	//	{
	//		TRACE( " %i %i,", m_arrRoadNodes[q].m_nCard, m_arrRoadNodes[q].m_nReverse );
	//	}
	//	TRACE( "\n" );
	//#endif
}

// 读端点表数据
void Ground::loadEndPoint()
{
	// 20181122 改回主表Point中的xCurrent和zCurrent，因为散托和换色号时有一些空托
	CString strsql = L"SELECT "
		L"  u.`type`"		// 0
		L", u.`id`"
		L", u.`code`"
		L", u.`serial`"
		L", u.`nextCode`"

		//L", u.`length`/v.`dx` as unsigned"	// 5	// 表达式返回的是字符串
		L", u.`zNumber`"	// 20191025 改为u 端点表中设置的层数优先
		L", u.`xCurrent`"
		L", u.`zCurrent`"
		L", u.`middleID`"

		L", u.`middleDis`"	// 10
		L", u.`dx`"			// point表中的间距
		L", v.`height`"		// ?? 与下面的表重复了，留做举升高度用
		L", u.`roadID`"
		L", u.`PLCIP`"

		L", u.`PLCPort`"	// 15
		L", u.`PLCAdd`"
		L", u.`node`"
		L", u.`delete`"
		L", u.`trayID`"

		L", u.`length`"		// 20
		L", v.`width`"		// 变成v.
		L", v.`depth`"
		L", v.`height`"
		L", u.`angle`"

		L", v.`number`"		// 25
		L", u.`disable`"
		L", v.`dx`"			// 托类型表中的最小间距
		L", u.`interface`"			// 托类型表中的最小间距

		L" FROM `point` u,`tray` v"		// 按id倒排序，以便后面分组合并时取最后一个有效值
		L" WHERE v.`id` = u.`trayID` and u.`delete` = 0"
		L" ORDER BY u.id;"
		;

	HRecordset rs( theApp.getDatabase() );
	rs.Open2( strsql );

	m_arrEndPoint.SetSize( 0, 200 );	// 预留100个位置，避免内存碎片

	// 取字段名
	//CODBCFieldInfo fi;
	//rs.GetODBCFieldInfo( (short)5, fi );

	//if( rs.GetRecordCount() > 0 )
	//	rs.MoveFirst();


	INT_PTR nLenRoad = m_arrRoadNodes.GetCount();

	CDBVariant var;	// var 要释放
	for( int i = 0; !rs.IsEOF(); i++)
	{
		int ii = 0;
		
		// m_nType 站点类型，bit0 充电，bit1 执行等待任务，bit2 取货，bit3 存货，bit4 电梯，bit5 过门，bit6 发车点，bit7 外部控制
		rs.GetFieldValue( (short)ii++, var );
		EndPoint::PointType pt;
		pt.m_nType = var.m_lVal;
		EndPoint * pep = 0;
		if(pt.m_bExtCtrl)
			pep = (EndPoint*)new ProductPoint();	// PLC控制
		else
			pep = (EndPoint*)new StorePoint();		// 

		/*POINT_TYPE nn = (POINT_TYPE)var.m_lVal;	// 端点类型
		EndPoint * pep = 0;
		switch (nn)
		{
		case POINT_TYPE::POINT_START:
			pep = (EndPoint*)new StartPoint2();		// 0发车点
			break;
		case POINT_TYPE::POINT_RECHARGE:
			pep = (EndPoint*)new RechargePoint();	// 1充电点
			break;
		case POINT_TYPE::POINT_WAIT:
			pep = (EndPoint *)new WaitPoint();		// 2等待点
			break;
		case POINT_TYPE::POINT_PRODUCT:
			pep = (EndPoint *)new ProductPoint();	// 3工位呼叫点
			break;
		case POINT_TYPE::POINT_STORE:
			pep = (EndPoint *)new StorePoint();		// 4库区
			break;
		case POINT_TYPE::POINT_LIFT:
			pep = (EndPoint*)new LiftPoint();		// 5电梯
			break;
		case POINT_TYPE::POINT_MIDDLE:
			pep = (EndPoint*)new MiddlePoint();		// 6等待点
			break;
		case POINT_TYPE::POINT_SPECIFY:
			pep = (EndPoint*)new SpecifyPoint();	// 7呼叫后指定存货点
			break;
		default:
			CString str;
			str.Format(L"端点类型（type）错误（%i），应该是0-7", nn);
			THROW(new HException(str));
			break;
		}
		*/

		m_arrEndPoint.Add( pep );
		addCtrl(pep);	// 在HCtrl容器中自动销毁pp	//?? 没有定客户区，默认为EndPoint::EndPoint(RoadNode & rn, int nRoadIndex)	: HCtrl(CRect(0, 0, 16, 16))

		pep->m_nType = pt.m_nType;

		rs.GetFieldValue((short)ii++, var);
		pep->setPointID(var.m_lVal);	// point表索引
		
		rs.GetFieldValue((short)ii++, var);
		if(var.m_pstring->GetLength() == 4)		// 必须是4个字符，8字节
			pep->setCode(*var.m_pstring);		// 端点类型，4个UNICODE二字节字符，前两个字符是大类，后两个字符是小类，RG**充电，WT**等待，取货，存货等
		else
		{
			CString str;
			str.Format(L"端点编码（code）错误（%s）应该4个字符", (LPCTSTR)*var.m_pstring);
			THROW(new HException(str));
		}

		rs.GetFieldValue((short)ii++, var);
		pep->m_nSerial = var.m_lVal;	// 序号，有可能相同，

		if (pep->m_nSerial > 16 || theApp.m_bAutoStore)			// 20200824 重庆唯美 从8改为16
			theApp.m_bOnlyCall = TRUE;		// ?? 20191119 库区较多时只呼叫，不控制道

		rs.GetFieldValue((short)ii++, var);
		if (var.m_dwType != DBVT_NULL)
			pep->m_strNextCode = *var.m_pstring;	// 下一级类型，可以有多个，每四个字符一个

		// 5
		//rs.GetFieldValue( (short)ii++, var );
		//ASSERT(var.m_dwType != DBVT_NULL);
		//pep->setXNumber(var.m_lVal);	// 最大水平垛数
		
		rs.GetFieldValue( (short)ii++, var );
		pep->setZNumber(var.m_lVal);	// 最大垂直垛数

		rs.GetFieldValue( (short)ii++, var );
		if (var.m_dwType != DBVT_NULL)
			pep->setXCurrent(var.m_lVal);	// 当前水平垛数

		rs.GetFieldValue( (short)ii++, var );
		if (var.m_dwType != DBVT_NULL)
			pep->setZCurrent(var.m_lVal);	// 当前垂直垛数

		rs.GetFieldValue( (short)ii++, var );
		if (var.m_dwType == DBVT_NULL)
			THROW(new HException(L"8001 数据库错误，端点表中没有等待点号为NULL"));
		if (var.m_lVal > nLenRoad)
			THROW(new HException(L"8001 数据库错误，端点表中的等待点号超限"));

		pep->m_nMiddleID = var.m_lVal;	// 中间点
		pep->m_nMiddleCard = var.m_lVal
			? m_arrRoadNodes[(UINT64)(var.m_lVal) - 1].m_nCard
			: 0;	// ID转换成卡号


		// 10
		rs.GetFieldValue((short)ii++, var);
		pep->m_nMiddleDis = var.m_lVal;	// 距存货中间点的等待距离

		rs.GetFieldValue( (short)ii++, var );
		pep->setDX(var.m_lVal);	// 垛间距

		rs.GetFieldValue( (short)ii++, var );
		pep->setDZ(var.m_lVal);	// 垛高

		rs.GetFieldValue( (short)ii++, var );
		if( var.m_dwType == DBVT_NULL )
			THROW( new HException( L"8001 数据库错误，端点表中没有节点索引" ) );
		if(var.m_lVal > nLenRoad)
			THROW(new HException(L"8001 数据库错误，端点表中的路号超限"));
		pep->setRoadNode(&m_arrRoadNodes[(INT_PTR)var.m_lVal - 1]);	// roads表索引，可能对应多个roadID，取其中一个，用于画图时计算位置和角度。

		rs.GetFieldValue( (short)ii++, var );
		if( var.m_dwType != DBVT_NULL )
			pep->setPLCIP(*var.m_pstring);	// 端点PLC IP地址

		// 15
		rs.GetFieldValue( (short)ii++, var );
		if( var.m_dwType != DBVT_NULL )
			pep->setPLCPort(var.m_lVal);	// 端点PLC端口

		rs.GetFieldValue((short)ii++, var);
		if (var.m_dwType != DBVT_NULL)
			pep->setPLCAdd(var.m_lVal);	// 端点对应的PLC内存地址，指向PLC W内存，两个字32位对应一个端点，最多32个开关量。

		ii++;	// 备注没有取值

		rs.GetFieldValue((short)ii++, var);
		if (var.m_dwType != DBVT_NULL)
			pep->m_bDelete = var.m_boolVal;	// 删除，删除后仍然显示

		rs.GetFieldValue((short)ii++, var);
		pep->m_nTrayID = var.m_lVal;	// 托号

		// 20
		rs.GetFieldValue((short)ii++, var);
		pep->m_nLength = var.m_lVal;	// 码货长度mm

//		rs.GetFieldValue((short)ii++, var);
//		pep->m_nTrayWidth = var.m_lVal;	// 托宽度mm

//		rs.GetFieldValue((short)ii++, var);
//		pep->m_nTrayDepth = var.m_lVal;	// 托深度mm

//		rs.GetFieldValue((short)ii++, var);
//		pep->m_nTrayHeight = var.m_lVal;	// 托高度mm

		//rs.GetFieldValue((short)ii++, var);
		//pep->m_nAngle = var.m_lVal;	// w
		ii += 4;

		// 25
		rs.GetFieldValue((short)ii++, var);
		ASSERT(!(var.m_lVal & 0xffff0000));
		pep->m_nTray = (WORD)var.m_lVal;	// v

		rs.GetFieldValue((short)ii++, var);
		pep->m_nPointState = (BYTE)var.m_lVal;	// 停用/起用

		rs.GetFieldValue((short)ii++, var);	// 类型库中定义的最小垛距
		if (var.m_lVal > pep->m_nDX)
			pep->setDX(var.m_lVal);

		rs.GetFieldValue((short)ii++, var);
		pep->m_nInterface = var.m_lVal;	// 停用/起用

		// 获取托对象
		pep->m_pTrayRecord = theApp.m_tray.getRecord((WORD)pep->m_nTray, (LPCTSTR)0);

		// 计算最大垛数
		pep->setXNumber(pep->m_nLength / pep->m_nDX);

		rs.MoveNext();
	}
	var.Clear();	// var 要释放
	rs.Close();

	size_t length = m_arrEndPoint.GetCount();
	if( !length )
	{
		CString str;
		str.Format( L"没有端点数据" );
		THROW( new HException( str ) );
		return;
	}

	// 初始化托
	for (size_t i = 0; i < length; i++)
	{
		EndPoint& ep = *m_arrEndPoint[i];
		ep.m_pTrayRecord = theApp.m_tray.getRecord((WORD)ep.m_nTray, (LPCTSTR)0);
	}
}

// 读AGV数据
void Ground::loadCars()
{
	/*CString speed;
	theApp.getSys( L"carspeed", speed );
	if( speed.IsEmpty() )
		THROW( new HException(L"无法读取AGV速度" ) );

	m_fSpeed = _wtof( speed );
	*/

	CString strsql =
		//L"SELECT `id`, `name`, `model`, `IP`, `port`, `outTime`, `disabled`,"
		//L" `state`, `productModel`, `productNumber`, `productGrade`, `productID`,"
		//L" `load`"
		//L" FROM `car` WHERE `deleted` = false order by `id`;";

	L"SELECT `car`.`id`,"			// 0
		L" `car`.`name`,"
		L" `car`.`model`,"
		L" `car`.`IP`,"
		L" `car`.`port`,"
		
		L" `car`.`outTime`,"			// 5
		L" `car`.`disabled`,"
		//L" `car`.`deleted`,"
		L" `car`.`state`,"
		L" `car`.`productModel`,"
		L" `car`.`productNumber`,"


		L" `car`.`productGrade`,"		// 10
		L" `car`.`productID`,"
		L" `car`.`load`,"
		L" `carmodel`.`model`,"
		L" `carmodel`.`name`,"
		
		L" `carmodel`.`type`,"			// 15
		L" `carmodel`.`load`,"
		L" `carmodel`.`length`,"
		L" `carmodel`.`width`,"
		L" `carmodel`.`height`,"
		
		
		L" `carmodel`.`x0`,"			// 20
		L" `carmodel`.`y0`,"
		L" `carmodel`.`cd1x`,"
		L" `carmodel`.`cd1y`,"
		L" `carmodel`.`cd2x`,"
		
		L" `carmodel`.`cd2y`,"			// 25
		L" `carmodel`.`nav1x`,"
		L" `carmodel`.`nav1y`,"
		L" `carmodel`.`nav2x`,"
		L" `carmodel`.`nav2y`,"
		
		L" `carmodel`.`dri1x`,"			// 30
		L" `carmodel`.`dri1y`,"
		L" `carmodel`.`dri2x`,"
		L" `carmodel`.`dri2y`,"
		L" `carmodel`.`wheeld`,"

		L" `car`.`line`,"				// 35
		L" `car`.`floor`"				// 36

		L" FROM `car` left join `carmodel`"
		L" ON `car`.`model` = `carmodel`.`model`"
		L" WHERE `car`.`deleted` = false order by `car`.`id`;";
		;

	HRecordset rs( theApp.getDatabase() );
	rs.Open2( strsql );

	m_arrCars.SetSize( 0, 32 );	// 预留32个位置，避免内存碎片

	// 取表的字段名
	//CODBCFieldInfo fi;
	//rs.GetODBCFieldInfo( (short)5, fi );

	CDBVariant var;	// var 要释放
	for( int i = 0; !rs.IsEOF(); i++ )
	{
		// 根据类型创建不同的AGV
		rs.GetFieldValue((short)15, var);	// 第15列是类型
		int nType = var.m_dwType == DBVT_LONG ? var.m_lVal : 0;
		Car* pCar = 0;
		switch (nType)
		{
		case 2:		// 潜伏2吨
			pCar = new Car2();
			break;

		default:
			pCar = new Car();
			break;
		}

		pCar->m_nType = nType;

		// 读数据
		// id
		rs.GetFieldValue( (short)0, var );
		pCar->setID( var.m_lVal );

		// name 名称
		CString str;
		rs.GetFieldValue( (short)1, str );
		pCar->setName( str );

		// model 型号
		rs.GetFieldValue( (short)2, str );
		pCar->setModel( str );

		// IP	//?? 格式验证
		rs.GetFieldValue( (short)3, str );
		pCar->setIP( str );

		// port 端口	//?? 格式验证
		rs.GetFieldValue( (short)4, var );
		pCar->setPort( var.m_lVal );

		// outTime 超时 //???? 在这儿设置还是在选项中设置？
		//rs.GetFieldValue( (short)5, var );
		//pCar->setOutTime( var.m_lVal );

		// disabled 停用
		rs.GetFieldValue( (short)6, var );
		ASSERT( var.m_dwType == DBVT_UCHAR );	// 实测MySQL数据库中TINYINT(1)，对应的类型是DBVT_UCHAR，对应MFC var.m_chVal，不是m_boolVal
												// https://msdn.microsoft.com/zh-cn/library/4hhd4f44(v=vs.100).aspx
		pCar->setDisabled( var.m_chVal );


		// 20181119 新加

		// 状态
		rs.GetFieldValue((short)7, var);
		//pCar->m_bLoaded = !!(var.m_lVal & CS_LOADED);			// AGV状态，第0位为1时表示有货，存入路径表中
		//pCar->m_bHalfProduct = !!(var.m_lVal & CS_HALF);		// 散托
		//pCar->m_bChangeColor = !!(var.m_lVal & CS_CHANGE);	// 换色号
		pCar->m_carData.nState = var.m_lVal;
		pCar->m_bLoaded = pCar->m_carData.bLoad;
		// 货物规格型号
		rs.GetFieldValue((short)8, var);
		pCar->m_strProductModel = *var.m_pstring;
		// 货物规格型号
		rs.GetFieldValue((short)9, var);
		pCar->m_strProductNumber = *var.m_pstring;
		// 货物规格型号
		rs.GetFieldValue((short)10, var);
		pCar->m_strProductGrade = *var.m_pstring;
		// 货物规格型号
		rs.GetFieldValue((short)11, var);
		pCar->m_strProductID = *var.m_pstring;
		// 载重
		rs.GetFieldValue((short)12, var);
		pCar->m_nLoad = var.m_lVal;
		// 对应线号
		rs.GetFieldValue((short)35, pCar->m_strLine);
		// 工用区域
		rs.GetFieldValue((short)36, var);
		pCar->m_nFloor = var.m_lVal;

		// AGV参数
		if (nType)
		{
			// m_f长度
			rs.GetFieldValue((short)17, var);
			pCar->m_fAGV长 = (float)var.m_lVal;

			// m_fAGV宽
			rs.GetFieldValue((short)18, var);
			pCar->m_fAGV宽 = (float)var.m_lVal;

			// m_f磁点位置x
			rs.GetFieldValue((short)22, var);
			// pCar->m_f磁点位置x = (float)var.m_lVal;
			pCar->m_f前长 = (float)var.m_lVal;					// 激光AGV，定义为AGV前长，叉尖到被动轮
			pCar->m_f后长 = pCar->m_f前长 - pCar->m_fAGV长;		// 负数

			// m_f磁点位置y
			rs.GetFieldValue((short)23, var);
			pCar->m_f磁点位置y = (float)var.m_lVal;

			// m_f磁点位置x
			rs.GetFieldValue((short)24, var);
			pCar->m_f货叉基点 = (float)var.m_lVal;					// 激光AGV，定义为AGV前长，叉尖到被动轮

			// m_f导航器位置x
			rs.GetFieldValue((short)26, var);
			pCar->m_f导航器位置x = (float)var.m_lVal;

			// m_f导航器位置y
			rs.GetFieldValue((short)27, var);
			pCar->m_f导航器位置y = (float)var.m_lVal;

			// m_f驱动轮位置x
			rs.GetFieldValue((short)30, var);
			pCar->m_f驱动轮位置x = (float)var.m_lVal;

			// m_f驱动轮位置y
			rs.GetFieldValue((short)31, var);
			pCar->m_f驱动轮位置y = (float)var.m_lVal;

			// m_f驱动轮直径
			rs.GetFieldValue((short)34, var);
			pCar->m_f驱动轮直径 = (float)var.m_lVal;

			// 求最大长度
			int length = (int)abs( pCar->m_f前长 - pCar->m_f后长 );
			if (Car::MaxLength < length)
				Car::MaxLength = length;
		}

		Car::MaxLength += 1000;

		//pCar->setNodes( m_arrRoadNodes[n1], m_arrRoadNodes[n2] );
		//pCar->setNodes( &m_arrRecharges[i] );

		/*int n = getNode( road, serial );
		if( n + 1 >= m_arrRoadNodes.GetCount() ||
			m_arrRoadNodes[n].m_nRoad != m_arrRoadNodes[n+1].m_nRoad
			)
			THROW( new HException(L"AGV节点在道路末端" ) );	// ???? 暂时用异常处理，以后改用报警提示，保证系统正常运行。
		pCar->setNode1( m_arrRoadNodes[n] );
		pCar->setNode2( m_arrRoadNodes[n+1] );
		*/

		//pCar->setDistance( distance );

		// 20170722 路径颜色，采用保合度和亮度不变，改变颜色参见PS和http://blog.csdn.net/xhhjin/article/details/7020449
		float hh = i * 60.f + 10;
		for (; hh >= 360; hh -= 360);	// 色调小于360
		D2D1::ColorF col = HSVtoRGB(hh, 1.f, 1.f);
		col.a = 0.7f;

		pCar->setBrush( new CD2DSolidColorBrush(&theApp.m_RenderTarget, col) );

		m_carList[pCar->m_nFinsNode] = pCar;
		m_arrCars.Add( pCar );
		addCtrl( pCar );

		rs.MoveNext();
	}
	var.Clear();	// var 要释放
	rs.Close();

	INT_PTR n = m_arrCars.GetCount();
	if( !n )
		THROW( new HException(L"没有AGV数据" ) );

	//if( n > 32 )
	//	THROW( new HException(L"最多只能有32个AGV（PLC通讯编号要求）" ) );
}

/// <类型>Fanction</类型>
/// <作者>Mr. Hu</作者>
/// <日期>20190214</日期>
/// <名称>loadTurnDis</名称>
/// <功能>读转弯前行表</功能>
/// <参数 name = ""></参数>
/// <返回></返回>
/// <说明>读转弯前行表</说明>
void Ground::loadTurnDis()
{
	CString strsql =
		L"SELECT `id`, `card1`, `card2`, `card3`, `type`, `dis`, `base` FROM `turndis`;";
	HRecordset rs(theApp.getDatabase());
	rs.Open2(strsql);

	m_arrTurnDis.SetSize(0, 32);	// 预留32个位置，避免内存碎片

	// 取表的字段名
	//CODBCFieldInfo fi;
	//rs.GetODBCFieldInfo( (short)5, fi );

	CDBVariant var;	// var 要释放
	for (int i = 0; !rs.IsEOF(); i++)
	{
		TurnDis turnDis;

		// 读数据 ?? 异常
		// id
		rs.GetFieldValue((short)0, var);
		turnDis.nID = var.m_lVal;
		// 前一点
		rs.GetFieldValue((short)1, var);
		turnDis.nCard1 = var.m_lVal;
		// 当前点
		rs.GetFieldValue((short)2, var);
		turnDis.nCard2 = var.m_lVal;
		// 后一点
		rs.GetFieldValue((short)3, var);
		turnDis.nCard3 = var.m_lVal;
		// 类型
		rs.GetFieldValue((short)4, var);
		turnDis.nType = var.m_lVal;
		// 前行距离
		rs.GetFieldValue((short)5, var);
		turnDis.nDis = var.m_lVal;
		// 前行距离
		rs.GetFieldValue((short)6, var);
		turnDis.nBase = var.m_lVal;

		if (turnDis.nDis > theApp.m_nMaxTurnDis || turnDis.nDis < -theApp.m_nMaxTurnDis)
		{
			CString str;
			str.Format(L"转弯前进距离(%i)应该在正负%i之间。)", turnDis.nDis, theApp.m_nMaxTurnDis);
			AfxMessageBox(str);
			continue;
		}
		else
			m_arrTurnDis.Add(turnDis);

		rs.MoveNext();
	}
	var.Clear();	// var 要释放
	rs.Close();
}

//计算顶级控件位置。
void Ground::layerout()
{
	HCtrl::layerout();

	// 计算旋转后的场地长宽，按指定角度建立旋转矩阵
	D2D1::Matrix3x2F mtx = D2D1::Matrix3x2F::Rotation( theApp.m_fDirection );
	// 求边界
	int minX = MAXINT, minY = MAXINT, maxX = MININT, maxY = MININT;
	for(INT_PTR i = m_arrRoadNodes.GetCount() - 1; i >=0; i-- )
	{
		RoadNode & rn = m_arrRoadNodes[i];
		int x = int(rn.m_nX * mtx._11 + rn.m_nY * mtx._21);	// 计算新坐标
        int y = int(rn.m_nX * mtx._12 + rn.m_nY * mtx._22);
		if( minX > x )
			minX = x;
		if( maxX < x )
			maxX = x;
		if( minY > y )
			minY = y;
		if( maxY < y )
			maxY = y;
	}
	// 计算宽度高度
	int nWidth = maxX - minX + Car::MaxLength;
	int nHeight = maxY - minY + Car::MaxLength;

	// 计算地图宽度高度
	CRect rc2 = m_rcClient;
	rc2.DeflateRect(m_nPadding, m_nPadding);
	CRect rc = putRect(rc2, nWidth, nHeight);

	// 坐标变换
	int w = rc.Width();
	float fScale = (float)w / nWidth * m_fWheelScale;
	float x = float(m_nPadding + Car::MaxLength / 2 * fScale + rc.left * 2.f - minX * fScale);	// rc.left * 1.8 靠右，导航图零点的屏幕坐标
	float y = float(m_nPadding + Car::MaxLength / 2 * fScale + rc.top * 2.f - minY * fScale);		// rc.top * 1.8 靠下，导航图零点的屏幕坐标
	if( m_fWheelScale > 1 )			// 如果大于1，计算放大后的坐标，鼠标点不动。
	{
		float s = fScale / theApp.m_mtxRoad.m_fScale;	// 本次的缩放比例
		x = theApp.m_mtxRoad._31 * s - m_fWheelX * s + m_fWheelX;	// 求地图原点相对屏幕的坐标
		y = theApp.m_mtxRoad._32 * s - m_fWheelY * s + m_fWheelY;	// 求地图原点相对屏幕的坐标
	}

	// 设置变换矩阵，包括旋转
	matrix3x2f & mtx2 = theApp.m_mtxRoad;	// 用临时变量替换和直接用theApp.m_mtxRoad编译结果完全一样
	float ang = theApp.m_fDirection * M_DEG1;
	mtx2.m_fScale = fScale;
	mtx2._11 = mtx2.m_fScale * (float)cos( ang );
	mtx2._12 = mtx2.m_fScale * (float)sin( ang );
	mtx2._21 = -mtx2._12;
	mtx2._22 = mtx2._11;	// 垂直方向负比例，上下颠倒
	mtx2._31 = x;// * m_fWheelScale - (m_fWheelScale - 1) * m_fWheelX;
	mtx2._32 = y;// * m_fWheelScale - (m_fWheelScale - 1) * m_fWheelY;

	// 设置产品点变换矩阵
	for(INT_PTR i = 0, n = m_arrEndPoint.GetCount(); i < n; i++ )
		m_arrEndPoint[i]->setMatrix(theApp.m_mtxRoad );	// 根据场地矩阵计算产品点矩阵

	float s = theApp.m_mtxRoad.m_fScale / ((m_fWheelScale - 1) / 4 + 1);	// 文字适当放大
	if (s > 0.04f)
		s = 0.04f;
	//float s = m_mtxRoad.m_fScale;	// 文字适当放大

	if(theApp.m_pRoadTextFormat )
		delete theApp.m_pRoadTextFormat;
	theApp.m_pRoadTextFormat = new CD2DTextFormat(&theApp.m_RenderTarget, _T("宋体"), 14.f / s);	// 资源会自动销毁

	if(theApp.m_pPointTextFormat )
		delete theApp.m_pPointTextFormat;
	theApp.m_pPointTextFormat = new CD2DTextFormat(&theApp.m_RenderTarget, _T("Verdana"), 2.5f / s);	// 资源会自动销毁

	if(theApp.m_pRedTextFormat )
		delete theApp.m_pRedTextFormat;
	theApp.m_pRedTextFormat = new CD2DTextFormat(&theApp.m_RenderTarget, _T("Verdana"), 14.f / s);	// 资源会自动销毁

	m_bDrawBackground = TRUE;	// 重画背景
}

// 画道路
void Ground::draw( CDC* pDC, CRect & rcClip )
{
	HCtrl::draw( pDC, rcClip );

	//TimerCounter tc;

	if (theApp.m_bShowRunData)
	{
		// 显示端点PLC数据
		CString str;
		int add = -1;
		INT_PTR len = m_arrPointSocket.GetCount();
		for (INT_PTR k = 0; k < len; k++)
		{
			PointSocket* pps = m_arrPointSocket[k];
			//str.AppendFormat(L"Call %i %i\r\n", pps->m_nFinsNode, pps->getTick());

			CArray<EndPoint*>& ap = pps->m_arrPoint;
			INT_PTR len2 = ap.GetCount();
			for (INT_PTR m = 0; m < len2; m++)
			{
				PWORD pData = (PWORD)pps->m_plcData;
				PWORD pWrite = (PWORD)pps->m_plcWrite;
				int i = ap[m]->m_nPLCAdd;
				if (i == add)
					continue;													// 排除地址重复的数据，有可能多个站点用同一地址
				else
					add = i;

				int n = i + 10;													// 上传数据双字长度
				for ( ; i < n; )												// 显示h00 - h39
				{
					str.AppendFormat(L"00%02i：", i);
					for (int j = 0; j < 10 && i < n; j++, i++)
					{
						str.AppendFormat(L"%04X ", pData[i]);
						if (j == 4)
							str += L" ";
					}
					str.Append(L"\r\n");
				}

				i = ap[m]->m_nPLCAdd;
				for ( ; i < n; )	// 显示h00 - h39
				{
					str.AppendFormat(L"01%02i：", i);
					for (int j = 0; j < 10 && i < n; j++, i++)
					{
						str.AppendFormat(L"%04X ", pWrite[i]);
						if (j == 4)
							str += L" ";
					}
					str.Append(L"\r\n");
				}
			}

			//str.Append( L"\r\n" );
		}

		// 显示AGV PLC数据
		len = m_arrCars.GetCount();
		for (INT_PTR k = 0; k < len; k++)
		{
			str.Append(m_arrCars[k]->getPlcStr());
			//str.Append( L"\r\n" );
		}

		pDC->SetBkMode(TRANSPARENT);

		CFont* pOldfont = 0;
		CFont* pfont = getFont();
		if (pfont)
			pOldfont = pDC->SelectObject(pfont);

		COLORREF oldColor = pDC->SetTextColor(m_coText);	// 文字颜色

		CRect rc = m_rcWnd;
		rc.top += 8;
		rc.left += 4;
		pDC->DrawText(str, rc, NULL);

		pDC->SetTextColor(oldColor);

		if (pOldfont)
			pDC->SelectObject(pOldfont);
	}
	//tc.getCounter();	// 4ms

		
	/********************
	  Driect2D
	*********************/
	//
    // Draw the pie chart with Direct2D.
    //

	if (!theApp.m_bShowRoads)	// 不显示道路 // ?? 退出时异常
		return;

	
	HLock hGeoCreate(&afxMutexGeo, 1);


	//TimerCounter tc;
	// Bind the DC to the DC render target.
	theApp.m_RenderTarget.BindDC(*pDC, m_rcWnd);
	//tc.getCounter();		// 0.15ms

	//运行到m_RenderTarget.BeginDraw(); 时出现异常
	//	0x75D2AA12 处(位于 MFCGdiInteropSample.exe 中)引发的异常: Microsoft C++ 异常 : _com_error，位于内存位置 0x00F8E980 处。
	//	VS2010SP1自带的例子也出现这个问题
	//	C:\Program Files (x86)\Microsoft Visual Studio 10.0\Samples\2052\VC2010SP1Samples.zip\C++\MFC\D2D\MFCGdiInteropSample
	//TimerCounter tc;
	theApp.m_RenderTarget.BeginDraw();	// 开始画图，完成后必需用EndDraw
	//tc.getCounter();		// ?? 7ms 产生一个异常，占用了较多时间，想办法解决 0x00007FFC13BCA388 处(位于 GHCY.exe 中)引发的异常: Microsoft C++ 异常: _com_error，位于内存位置 0x000000C1258FE040 处。
							// 20180823 4ms V15.8.1 好象没有这个问题了
							// 20180824 还是有异常问题，单显示器没有，双显示器时有问题。
	//TimerCounter tc;

	// 画道路
	if (m_bDrawBackground)	// 重画背景
	{
		m_bDrawBackground = FALSE;	// 多线程的原因，这一句放在开始，避免下面的程序执行中又来一个线程
		if (m_pBrushGround)
			clearBackground();
		m_pBrushGround = 0;	// 多线程的原因，先置0
		m_pBrushGround = createBackground();	// 创建背景位图
	}

	theApp.m_RenderTarget.SetTransform(D2D1::IdentityMatrix());	// 坐标变换

	ASSERT(m_pBrushGround);
	//ASSERT(m_pBrushGround->IsValid());

	theApp.m_RenderTarget.FillRectangle( CD2DRectF(0, 0, (float)m_rcClient.Width(), (float)m_rcClient.Height()), m_pBrushGround);

	//tc.getCounter();		// 准备好资源后 0.01ms，直接画 6ms

	
	// 画行驶路径
	for (INT_PTR i = 0, n = m_arrCars.GetCount(); i < n; i++)
	{
		m_arrCars[i]->drawPath();
	}

	//画货垛
	for(INT_PTR i = 0, n = m_arrEndPoint.GetCount(); i < n; i++ )
	{
		m_arrEndPoint[i]->drawPoint();
	}

	// 画AGV，改变了m_RenderTarget的变换矩阵，只能在最后
	for (INT_PTR i = 0, n = m_arrCars.GetCount(); i < n; i++)
	{
		m_arrCars[i]->drawAGV();
	}

	/*HRESULT hr = */
	theApp.m_RenderTarget.EndDraw();	// 结束画图	//?? 异常

	//tc.getCounter();	// 26ms
}

// 建立背景资源，提高效率
CD2DBitmapBrush* Ground::createBackground()
{
	CD2DSizeF sizef((float)m_rcClient.Width(), (float)m_rcClient.Height());
	CBitmapRenderTarget bitmapTarget;
	if (!theApp.m_RenderTarget.CreateCompatibleRenderTarget(bitmapTarget, sizef))
		return NULL;

	//TimerCounter tc;
	bitmapTarget.BeginDraw();	// 开始画图，完成后必需用EndDraw
	//tc.getCounter();			// 0.002ms

	// 20181016 清理背景并透明，能显示GDI中的数据
	bitmapTarget.Clear(D2D1::ColorF(D2D1::ColorF(0.0f, 0.0f, 0.0f, 0.0f)));

	// 画方向标识
	bitmapTarget.SetTransform(	// 坐标变换
		D2D1::Matrix3x2F::Rotation(theApp.m_fTurnDirection)	// 旋转30度
		* D2D1::Matrix3x2F::Translation(float(m_rcClient.Width()) - 50, 50)
	);
	111111111;	// 这个矩阵可能要先移后转
//// 方向标识取位图的0, 0, 100, 100
	//CD2DRectF rcf(0, 0, 100, 100);
	//ASSERT(theApp.m_pBmpGroup);
	//bitmapTarget.DrawBitmap(theApp.m_pBmpGroup, CRect(0, 0, 100, 100), 0.4f, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, &rcf);


	CD2DPathGeometry geoDriction(&theApp.m_RenderTarget);	// 方向标识
	geoDriction.Create(&theApp.m_RenderTarget);
	ASSERT(geoDriction.IsValid());	//???? 异常
	CD2DGeometrySink sink1(geoDriction);	//?? 内存会不会越来越多
	ASSERT(sink1.IsValid());	//???? 异常

	// 设置起点
	D2D1_POINT_2F pt = { 0, -20 };
	sink1.BeginFigure(pt, D2D1_FIGURE_BEGIN_FILLED);
	pt = { -5, 25 };
	sink1.AddLine(pt);
	pt = { 5, 25 };
	D2D1_SIZE_F si1 = { 15, 15 };
	sink1.AddArc(						// 大弧线
		D2D1::ArcSegment(
			pt,							// end point  
			si1,
			0.0f, // rotation angle  
			D2D1_SWEEP_DIRECTION_CLOCKWISE,
			D2D1_ARC_SIZE_LARGE
		));

	sink1.EndFigure(D2D1_FIGURE_END_CLOSED);
	sink1.Close();
	bitmapTarget.DrawGeometry(&geoDriction, theApp.m_pGroundBrush, 2.f);

	bitmapTarget.SetTransform(theApp.m_mtxRoad);	// 坐标变换

















	CD2DPathGeometry geoBezier(&theApp.m_RenderTarget);	// 方向标识
	geoBezier.Create(&theApp.m_RenderTarget);
	ASSERT(geoBezier.IsValid());	//???? 异常
	CD2DGeometrySink sink2(geoBezier);	//?? 内存会不会越来越多
	ASSERT(sink2.IsValid());	//???? 异常
	/*
	for (size_t i = 0; i < 35; i++)
	{
		pt = { 1500.f + i * 1400, 0 };
		sink2.BeginFigure(pt, D2D1_FIGURE_BEGIN_FILLED);
		D2D1_POINT_2F pt2 = { 2000 + i * 1400, 0 };
		D2D1_POINT_2F pt3 = { 2500 + i * 1400, 1000 };
		D2D1_POINT_2F pt4 = { 2500 + i * 1400, 2000 };
		D2D1_BEZIER_SEGMENT bs = { pt2, pt3, pt4 };
		sink2.AddBezier(bs);
		sink2.EndFigure(D2D1_FIGURE_END_OPEN);

		pt = { 1500.f + i * 1400, 0 };
		sink2.BeginFigure(pt, D2D1_FIGURE_BEGIN_FILLED);
		pt2 = { 2000.f + i * 1400, 0 };
		pt3 = { 2500.f + i * 1400, -1000 };
		pt4 = { 2500.f + i * 1400, -2000 };
		bs = { pt2, pt3, pt4 };
		sink2.AddBezier(bs);
		sink2.EndFigure(D2D1_FIGURE_END_OPEN);

		pt = { 3500.f + i * 1400, 0 };
		sink2.BeginFigure(pt, D2D1_FIGURE_BEGIN_FILLED);
		pt2 = { 3000.f + i * 1400, 0 };
		pt3 = { 2500.f + i * 1400, -1000 };
		pt4 = { 2500.f + i * 1400, -2000 };
		bs = { pt2, pt3, pt4 };
		sink2.AddBezier(bs);
		sink2.EndFigure(D2D1_FIGURE_END_OPEN);

		pt = { 3500.f + i * 1400, 0 };
		sink2.BeginFigure(pt, D2D1_FIGURE_BEGIN_FILLED);
		pt2 = { 3000.f + i * 1400, 0 };
		pt3 = { 2500.f + i * 1400, 1000 };
		pt4 = { 2500.f + i * 1400, 2000 };
		bs = { pt2, pt3, pt4 };
		sink2.AddBezier(bs);
		sink2.EndFigure(D2D1_FIGURE_END_OPEN);
	}
	*/

	sink2.Close();
	bitmapTarget.DrawGeometry(&geoBezier, theApp.m_pGroundBrush, 150.f);


















	//TimerCounter tc;

	//画场地
	INT_PTR road = -1;
	for (INT_PTR i = 0, n = m_arrGround.GetCount(); i < n; i++)
	{
		float x1 = (float)m_arrGround[i].m_nX;
		float y1 = (float)m_arrGround[i].m_nY;
		// 第一个点
		if (road != m_arrGround[i].m_nRoad)
		{
			road = m_arrGround[i].m_nRoad;
			continue;	// 如果是路起点，重新画
		}

		float x2 = (float)m_arrGround[i - 1].m_nX;
		float y2 = (float)m_arrGround[i - 1].m_nY;
		float w = 100.f;
		bitmapTarget.DrawLine(
			CD2DPointF(x2, y2),
			CD2DPointF(x1, y1),
			theApp.m_pGroundBrush,
			w
		);
	}

	//bitmapTarget.SetTransform(
	//	afxFloors.m_arrFloors[0].mtxAgv2Map
	//	* theApp.m_mtxRoad//);	// 坐标变换
	//);
	//画道路
	CD2DEllipse ell(	// 小圆圈
		CD2DPointF(0, 0),
		CD2DSizeF(100, 100)
	);
	road = -1;
	for (INT_PTR i = 0, n = m_arrRoadNodes.GetCount(); i < n; i++)
	{
		float s = theApp.m_mtxRoad.m_fScale / ((m_fWheelScale - 1) / 4 + 1);	// 文字框比例
		if (s > 0.04f)
			s = 0.04f;

		RoadNode& rn = m_arrRoadNodes[i];
		float x1 = (float)rn.m_nX;
		float y1 = (float)rn.m_nY;
		//UINT8 nn = afxFloors.m_bufFloors[rn.m_nFloor];
		//if (nn)
		//{
		//	D2D1::Matrix3x2F& mtx = afxFloors.m_arrFloors[nn].mtxAgv2Map;
		//	D2D1_POINT_2F pt = mtx.TransformPoint({ x1, y1 });		// 转换到终点
		//	x1 = pt.x;
		//	y1 = pt.y;
		//}

		float w = 120.f / s;
		float h = 10.f / s;

		// 地标卡号，锁定的AGV号，如果是端点，画端点类型号，
		//CString str;
		//int nCard = m_arrRoadNodes[i].m_nCard;
		CD2DTextFormat* pTextFormat = theApp.m_pRoadTextFormat;
		CD2DBrush* pTextBrush = theApp.m_pRoadBrush;
		//RoadNode & rn = m_arrRoadNodes[i];
		int nep = rn.m_nPointID;
		//EndPoint* pep = 0;
		CString str;
		if (nep)
		{
			ASSERT((INT_PTR)nep - 1 < m_arrEndPoint.GetCount());
			EndPoint* pep = m_arrEndPoint[(INT_PTR)nep - 1];
			if (pep->m_bDisabled)
			{
			//	str = L"停";
			//	pTextFormat = theApp.m_pRedTextFormat;
				pTextBrush = theApp.m_pRedBrush;
			}
			str = pep->getCode();
		}
		else if (theApp.m_bHexRoad && rn.m_nCard > 0)
			str.Format(L"%x ", rn.m_nCard);
		else
			str.Format(L"%i ", rn.m_nCard);

		//// 显示发车点
		//int ns = m_colls.isStartNode(rn.m_nID - 1);
		//if( ns >= 0 )
		//{
		//	// 加发车点，显示为红色
		//	str.AppendFormat(L"%i", ns);
		//	pTextBrush = theApp.m_pRedBrush;
		//}

		bitmapTarget.DrawText(str, CD2DRectF(x1, y1, x1 + w, y1 + h), pTextBrush, pTextFormat);

		// 第一个点
		w = 100;
		if (road != m_arrRoadNodes[i].m_nRoad)
		{
			ell.point.x = x1;
			ell.point.y = y1;
			bitmapTarget.DrawEllipse(ell, theApp.m_pRoadBrush, w);	// 第一点画个小圈
			road = m_arrRoadNodes[i].m_nRoad;
			// 路号
			//str.Format( L"%i", road );
			//float x3 = (x1 + m_arrRoadNodes[i+1].m_nX) / 2.f;	// 显示到第一段的中间
			//float y3 = (y1 + m_arrRoadNodes[i+1].m_nY) / 2.f;
			//if( x3 == x1 )
			//	x3 -= (road < 1000 ? road < 100 ? road < 10 ? 11.f : 22.f : 33.f : 44.f) / m_mtxRoad.m_fScale;
			//if( y3 == y1 )
			//	y3 -= 28.f / m_mtxRoad.m_fScale;
			//m_RenderTarget.DrawText(str, CD2DRectF( x3, y3, x3+w, y3+h ), m_pRoadTextBrush, m_pRoadTextFormat );

			continue;	// 如果是路起点，重新画
		}

		float x2 = (float)m_arrRoadNodes[i - 1].m_nX;
		float y2 = (float)m_arrRoadNodes[i - 1].m_nY;
		bitmapTarget.DrawLine(
			CD2DPointF(x2, y2),
			CD2DPointF(x1, y1),
			theApp.m_pRoadBrush,
			w
		);
	}

	// 端点激活标识，横码时不画
	for (INT_PTR i = 0, n = m_arrEndPoint.GetCount(); !theApp.m_bTransverse && i < n; i++)
	{
		EndPoint* pep = m_arrEndPoint[i];
		if (!pep->m_bActived)
			continue;	// 没有下一点，跳过

		float w = ell.radiusX * 8;
		RoadNode* prn = pep->m_pRoadNode;
		ell.point.x = (float)prn->m_nX;
		ell.point.y = (float)prn->m_nY;
		bitmapTarget.DrawEllipse(ell, theApp.m_pGreenBrush, w);

		/*EndPoint* pep = m_arrEndPoint[i];
		EndPoint* pep2 = pep->getNextPointAll();
		if (!pep2)
			continue;	// 没有下一点，跳过

		float w = ell.radiusX * 8;
		RoadNode* prn = pep2->m_pRoadNode;
		ell.point.x = (float)prn->m_nX;
		ell.point.y = (float)prn->m_nY;
		if (pep->getNextPoint())	// 一次存货
		{
			bitmapTarget.DrawEllipse(ell, theApp.m_pGreenBrush, w);
		}
		else  // 二次倒货取货点和存货点
		{
			bitmapTarget.DrawEllipse(ell, theApp.m_pRedBrush, w);

			ell.point.x = (float)pep->m_pRoadNode->m_nX;
			ell.point.y = (float)pep->m_pRoadNode->m_nY;
			bitmapTarget.DrawEllipse(ell, theApp.m_pRedBrush, ell.radiusX * 8);
		}*/
	}

	bitmapTarget.EndDraw();

	// Choose the tiling mode for the bitmap brush.
	//D2D1_BITMAP_BRUSH_PROPERTIES brushProperties =
	//	D2D1::BitmapBrushProperties(D2D1_EXTEND_MODE_WRAP, D2D1_EXTEND_MODE_WRAP);

	// Create the bitmap brush.
	CD2DBitmapBrush* pBrush = new CD2DBitmapBrush(&theApp.m_RenderTarget, NULL, NULL, TRUE);

	// Retrieve the bitmap from the render target.
	bitmapTarget.GetBitmap(*pBrush->GetBitmap());

	return pBrush;
}

// 取指定位置的节点序号
/*int Ground::getNodeIndex(int road, int serial)
{
	for( int i = 0, n = m_arrRoadNodes.GetCount(); i < n; i++ )
	{
		if( road == m_arrRoadNodes[i].m_nRoad &&
			serial == m_arrRoadNodes[i].m_nSerial
			)
		{
			return i;
		}
	}

	// ???? 暂时用异常处理，以后改用报警提示，保证系统正常运行。
	//"找不到 %i 号AGV所在节点，请手动复原该AGV"
	THROW( new HException( L"找不到AGV所在节点" ) );
}*/

// 查找前来取货的时间最短的AGV，可能正在存货过程中，只判断进入存货最后一段的AGV。
// 如果AGV正在等待，并且比最后一段存货的AGV快，返回这个AGV，否则返回0
// int nGet 取货点数组序号
Car * Ground::getNearCar(int nGet )
{
	INT_PTR n = m_arrCars.GetCount();
	int nDis = INT_MAX;
	Car * pCar = 0;
	RoadNode * prn = &m_arrRoadNodes[nGet];
	EndPoint* pep = prn->m_nPointID	// 中间点
		? m_arrEndPoint[(INT_PTR)prn->m_nPointID - 1]	// 取端点
		: getPointFromMidle(prn->m_nCard);		// 中间点对应的端点
	//ASSERT(pep && pep->IsKindOf(RUNTIME_CLASS(ProductPoint)));
	if (!pep)
		return 0;

	for (int i = 0; i < n; i++)
	{
		Car * pc = m_arrCars[i];

		if (pc->getDisable())
			continue;	// 停用
		
		ASSERT(pep->m_pTrayRecord);
		if (pep->IsKindOf(RUNTIME_CLASS(ProductPoint)) &&			// ?? 从库区取货，未考滤载重
			pep->m_pTrayRecord->nWeight != pc->m_nLoad)		// 如果是取产品，要求载重相同
		{
			continue;	// 载重不一样，跳过
		}

		if (pc->isLowBattery())
			continue;	// 亏电，跳过

		if (pc->getErrorCode() & 0x8000)
			continue;	// 故障

		//if (pc->m_pReadData->nState == 0x1025)	// 发车状态不算，因为在中间等待点发车会掉头到别处
		//	continue;

		HLock hl(&pc->m_mutexRun, -1, 0);	// 锁定运行锁，主要是防止找最近的AGV和AGV线程请求冲突	// ?? 可能漏到最近的AGV
		if (!hl.allowed())
			continue;	// 锁不住跳过

		HLock hl2(&pc->m_mutexPath, 1, (DWORD)-26);	// 等待读，退出时自动解锁

		INT_PTR ll = pc->m_arrPath.GetCount();
		ASSERT(ll >= 2);
		if (ll < 2)			// ?? 异常，正在使用的AGV路径是0
		{
			// 异常处理
			continue;
		}

		if (pc->m_arrPath[ll - 1].m_nID - 1 == nGet)
		{
			continue;	// 光华道路既是取货点又是存货点，排除自己占用的端点
		}

		// 如果占则跳过
		if (pc->m_nCarOccupy)
		{
			continue;
		}

		BOOL bWaitGet = pc->waitGet();

		// ?? 距离没按实际路径计算，可能有问题
		int d = INT_MAX;	// 距离
		if (bWaitGet)
		{
			d = pc->calDistance(prn);
		}
		//else if ((pc->m_pReadData->nState & 0x1080) == 0x1080)	// 存货路段
		//{
		//	d = (pc->m_nDisDest + pc->m_pReadData->sd1.nLength) * 2	// 推行时间
		//		+ ((pc->m_pReadData->sd1.nDirection) & 0x00ff) * 5000	// 码垛时间，pc->m_pReadData->nDirection1 层数，15000 15秒，// 20181130 低8位是层数
		//																// 20180413 每层时间改为5秒，存货时间长一些也取较近的，减少交叉
		//																// 减少拥堵
		//		+ pc->calDistance(prn);									// 到取货点的距离	// ?? 改东鹏程序
		//}
		else
			continue;

		// 如果是对应的存货点，优先选择，避免占道
		if (pc->waiting())
		{
			ASSERT(pc->m_arrPath[1].m_nPointID);
			EndPoint* pep2 = m_arrEndPoint[(INT_PTR)pc->m_arrPath[1].m_nPointID - 1];
			if (pep2->isParent(pep))
			{
				pCar = pc;
				break;
			}
		}

		// 取距离较近的
		if (d < nDis)
		{
			nDis = d;
			pCar = pc;
		}
	}

	//return pCar != 0 && pCar->waitGet()
	//	? pCar	// 选择的AGV正在等待取货，返回这个AGV
	//	: 0;	// 否则选中的AGV正在存货，返回0，存货完成后再启动
	return pCar;	// 20180105 正在运行也返回，外面的程序判断

/*
		//if( m_arrCars[i]->isMatch( pPoint ) )		// 如果车匹配。
		{
			RoadNode* prr = m_arrCars[i]->getNode();
			if( !prr )	// 未设置初始点，不显示
				continue;

			int ll = prr->calDistance( *pPoint );
			if( ll < nLen )
			{
				nLen = ll;
				nCar = i;
			}
		}
	}
	

	return 0;*/
}

/// <作者>Mr. Hu</作者>
/// <日期>20180507</日期>
/// <名称>interfere20</名称>
/// <参数 name = "Car * pCar">指定的AGV</参数>
/// <返回>int 1 避让，0 不避让</返回>
/// <说明>避让版本2，用区域的方法计算避让</说明>
//HMutex m_mutexCard1;					// 卡号锁，计算避让时互锁
int Ground::interfere20(Car * pCar)
{
	//HLock hlCard(&m_mutexCard1, -1, -1);	// 等待读，退出时自动解锁
	//HLock hlCard(&pCar->m_nutexAvoid, -1, -1);	// 等待读，退出时自动解锁

	ASSERT(pCar->m_nRunIndex);

	// 初始化，清除全部参数
	// 20201114 m_bCancelInter 各线程中调用时混乱，取消
	//for (INT_PTR i = 0, length = m_arrCars.GetCount(); i < length; i++)
	//{
	//	m_arrCars[i]->m_bCancelInter = FALSE;
	//}

	// 20210109 改为m_mutexGeometry
	// 上级已经锁定
	//HLock hgl(&pCar->m_mutexGeometry, 1, 0);	// 锁定节点几何形状锁，防止避让程序合用中发生变化，m_mutexGeometry
	//if (!hgl.allowed())
	//	return 0;	// ?? 锁不住跳过

	// 20220409 南基加条件，如果在终点，不计算避让
	// 只取长度，不用锁
	//INT_PTR len = pCar->m_arrPath.GetCount();
	//if ((pCar->m_nRunIndex == len - 1)
	//	&& ((pCar->m_pReadData->nState & 0x000f) == 0x0005)
	//	)
	//{
	//	return 0;
	//}

	// 锁定将要到的区域
	// 20220418 南基 取消车体判断
	// 20220726 南基 取消后，很远就会避让，再加上
	int r = interfere21(pCar);		// 用车体范围判断干涉
	ASSERT(r >= 0 && r <= 2);
	if (r == 1)
		return r;					// 避让，直接返回	//?? 没有设置pCar->m_nAvoidIndex
	//int r = 2;


	int r2 = interfere22(pCar);		// 计算本AGV路径与其它AGV机身和路径
	ASSERT(r2 >= 0 && r2 <= 3);
	if (r2 == 1)
		return r2;					// 避让，直接返回


	int r3 = interfere25(pCar);		// 计算终点区域是否被占用
	if(r3)
		return r3;


	// 排除1以后的逻辑关系
	// r r2 r'
	// 0 0  0  r
	// 0 1  1  r2
	// 0 3  3  r2
	// 2 0  2  r
	// 2 1  1  r2
	// 2 3  2  r

	// 20210626 加这一句，解决F:\AGV记录\避让问题\避让问题3.docx\2021年6月26日星期六
	if (r2 == 0
		&& r == 0		// 20220605 增加这个条件，解决《E:\AGV记录\避让问题\避让问题4.docx/2022年6月5日星期日 9：05》中1号不避让的问题
		)
		return 0;

	if (r == 0 && r2 == 3)
		r = 3;
	if (r < 2)
		return r;	// 运行或者避让，直接返回。有交点，继续判断。

	r = interfere24(pCar, r);
	return r;
}

/// <作者>Mr. Hu</作者>
/// <日期>20180525</日期>
/// <名称>interfere21</名称>
/// <功能>计算本AGV机身与其它AGV机身和路径点</功能>
/// <参数 name = "Car * pCar">指定的AGV</参数>
/// <返回>int 0 不干涉，1 干涉避让，2 干涉后续判断</返回>
/// <说明>计算本AGV扩展区与其它AGV机身干涉，避让，最高级，
///       与其它AGV路径点干涉，如果交点被占，避让，否则继续处理。</说明>
int Ground::interfere21(Car * pCar)
{
	//ASSERT(!pCar->isTurning());

	int nResult = 0;

	// 检查将要到的点
	//RoadNode rn;
	//{
		//HLock hl0(&pCar->m_mutexPath, 1, -1);	// ?? 本线程中调用，不锁
		//CArray<PathNode, PathNode&> & path1 = pCar->getPath();
		ASSERT(pCar->m_nRealIndex < pCar->m_arrPath.GetCount());
		PathNode& pn1 = pCar->m_arrPath[pCar->m_nRealIndex];
	//}
	


	INT_PTR nCars = m_arrCars.GetCount();
	for (INT_PTR i = 0; i < nCars; i++)
	{
		Car * pc = m_arrCars[i];
		if (pc->getDisable())	// 停用
			continue;
		if (pc == pCar)	// 排除自己
			continue;
		//if (pc->m_bCancelInter)	// 排除同逆向
		//	continue;

		// 20220712 解决《E:\AGV记录\避让问题\避让问题4.docx/2022年7月11日星期一 1422》问题，共两处
		// 20220716 把2倍改为3倍，解决《E:\AGV记录\避让问题\避让问题4.docx/2022年7月16日星期六 0716》问题，共两处
		// 20220809 ?? 注释，南基临时解决《E:\AGV记录\避让问题\避让问题4.docx/2022年8月9日星期二 0628》问题，共两处
		//int d = pc->calDistance(pCar);
		//if (d > 3 * theApp.m_nSafeDis)		// 2倍安全距离外的不计算
		//	continue;

		// 20181201 加判断通讯中断标识
		//BOOL b1 = pCar->calDistance(pc) > theApp.m_nExtSafe;	// 距离较远时不发送通讯中断停车，减少停车次数
		//if (!pCar->m_bCommunication && !b1)
		//	pCar->m_bCommunication = TRUE;

		//if (b1)	// 两个AGV距离较远，跳过	// 20190311注释，否则不避让，详见OneNote/GHCY/2019/2019/3/5 10:11
		//	continue;

		// 20180511 改用AGV实际占用区域判断，两个AGV车体干涉，立即停止
		// 求逆
		//ASSERT(pc->m_pGeoAGV);
		//ASSERT(pCar->m_pGeoAGV);
		//D2D1::Matrix3x2F mtx1 = pc->m_mtxCar;
		//BOOL bb = mtx1.Invert();
		//ASSERT(bb);
		//D2D1::Matrix3x2F mtx2 = pCar->m_mtxExt * mtx1;
		//// 计算本AGV扩展区与其它AGV机身干涉
		//D2D1_GEOMETRY_RELATION aa = pc->m_pGeoAGV->CompareWithGeometry(*pCar->m_pGeoAGV, mtx2, 100.f);	// 1000次 5ms
		//ASSERT(aa > 0 && aa <= 4);
		//if (aa != 1)	// 有干涉
		//{
		//	pCar->m_pAvoidCar = pc;
		//	pCar->m_nAvoidIndex = (INT_PTR)(pCar->m_nRealIndex) - 1;	// 如果是机身碰到，避让点选到最近
		//	return 1;
		//}
		// 求逆
		if (pc->m_pGeoEXT
			&& pCar->m_pGeoEXT2
			)
		{
			D2D1::Matrix3x2F mtx1 = pc->m_mtxExt;
			BOOL bb = mtx1.Invert();
			ASSERT(bb);
			D2D1::Matrix3x2F mtx2 = pCar->m_mtxExt * mtx1;
			// 计算本AGV扩展区与其它AGV机身干涉
			UINT aa = 0;
			{
				HLock hGeoExt(&afxMutexGeo, 1);	// 等待读，退出时自动解锁

				if (pc->m_pGeoEXT->GetFigureCount() && pCar->m_pGeoEXT2->GetFigureCount())
					aa = pCar->compareCar(pc);	// 1000次 5ms
			}
			
			if( (GETWORD(aa, 1) != 1)								// 停止区相交，避让
				|| ((GETWORD(aa, 0) != 1)							// 减速区相交
					&& !pc->m_returnWcs.bAvoid// !(pc->m_pReadData->sd.sd9.nCmd & 0x8000)			// 对方避让时不避让
					)
				)
			{
				pCar->m_pAvoidCar = pc;
				pCar->m_nAvoidIndex = (INT_PTR)(pCar->m_nRealIndex) - 1;	// 如果是机身碰到，避让点选到最近
				return 1;
			}
		}

		HLock hl(&pc->m_mutexPath, 1, 3);	// 等待读，退出时自动解锁
		if (!hl.allowed())			// 路径被别处锁定时跳过
			continue;

		// 20210109 改为m_mutexGeometry
		// 20200111 注销这一段，因为可能短时取消避让
		//HLock hgl(&pc->m_mutexGeometry, 1, 0);	// 锁定节点几何形状锁，防止避让程序合用中发生变化，m_mutexGeometry
		//if (!hgl.allowed())
		//	return 0;	// ?? 锁不住跳过

		// 计算本AGV机身与其它AGV路径干涉
		CArray<PathNode, PathNode&> & path2 = pc->getPath();
		INT_PTR k = 0;
		for (INT_PTR length = path2.GetCount() - 1; k < length; k++)	// 第1点和最后一点不判断
		{
			if (k < pc->m_nRealIndex && !pc->m_arrPath[k].m_pLockGeometry)
				continue;	// 已经过的点，没有锁定，跳过

			PathNode & pn2 = path2[k];
			if (pCar->calDistance(&pn2) > ((pCar->m_fGeoLength + theApp.m_nSafeDis)))
			{
				// 20220717 同向时直接跳出，解决《E:\AGV记录\避让问题\避让问题4.docx/2022年7月17日星期日 0640》问题
				// 20220717 恢复，因为出现《E:\AGV记录\避让问题\避让问题4.docx/2022年7月17日星期日 0949问题
				continue;	// 距离较远，跳过
				//break;
			}

			// 20220716 排除同向，解决《E:\AGV记录\避让问题\避让问题4.docx/2022年7月16日星期六 2158》问题
			if (pn1.m_nCard == pn2.m_nCard
				&& EQUAL(pn1.m_fRoadRad, pn2.m_fRoadRad, 0.001)		// 同向，跳过
				&& pn2.smallAngle()					// 20220811 对方AGV转弯时要避让
				)
			{
				break;								// 跳过同向AGB
			}

			HLock hGeoExt(&afxMutexGeo, 1);	// 等待读，退出时自动解锁

			// 判断AGV车体和防护区与路线转弯区交集，这里用扩展防护区
			UINT nr = pn2.compareCarExt(pCar, pCar->m_pGeoEXT2);
			if (nr == (D2D1_GEOMETRY_RELATION_DISJOINT | (D2D1_GEOMETRY_RELATION_DISJOINT << 16)))	// 高低字都是1，无干涉
				continue;	// 无干涉，继续

			// 20180607加条件，对方是避让就不避让，转弯时占用的范围较大有干涉也强行通过
			// ?? 强行通过可能有问题，虽然有机身判断，但是机身位置可能不准
			if ((GETWORD(nr, 1) != D2D1_GEOMETRY_RELATION_DISJOINT				//20240411 占美6吨加上 // 20220604，增加这个条件，扩展区干涉，解决问题《E:\AGV记录\避让问题\避让问题4.docx/2022年6月4日星期六》
																				// 20220712 取消这个条件解决《E:\AGV记录\避让问题\避让问题4.docx/2022年7月12日星期二 1010》问题，实测没有上面的问题
				|| GETWORD(nr, 0) != D2D1_GEOMETRY_RELATION_DISJOINT			// AGV车体干涉
				)
				&& !(pc->m_returnWcs.bAvoid)						// 对方避让时不避让，通过	// ?? 有风险
				&& (pc->m_carData.fSpeed)									// "E:\GIT\调度指挥\避让问题5.docx"2024年9月18日星期三 22:26
				&& pn2.m_pLockGeometry
				)
			{
				pCar->m_pAvoidCar = pc;
				pCar->m_nAvoidIndex = (INT_PTR)pCar->m_nRealIndex - 1;			// 如果是机身碰到，避让点选到最近
				return 1;	// ?? 返回之前一定要解锁
			}
			//else if (interfere23(pCar, nn2, pc, k, 2))	// 判断逆向干涉
			//{
			//	pCar->m_pAvoidCar = pc;
			//	pCar->m_nAvoidIndex = pCar->m_nRealIndex;	// 如果是机身碰到，避让点选到最近
			//	return 1;
			//}
			else
				nResult = 2;	// 有交叉，未锁定，待后续处理，pCar->m_nAvoidIndex 避让点后续处理
		}
	}

	// 20180701 注释这一段，pCar->m_roadNode是已经过的点，引起误判
	//if (nResult == 2)
	//{
	//	if (pCar->calDistance(&pCar->m_roadNode) > theApp.m_nSafeDis * 2)
	//		return 0;	// 如果本段路还较长，不避让
	//}

	ASSERT(nResult != 1);
	return nResult;
}

/// <作者>Mr. Hu</作者>
/// <日期>20180525</日期>
/// <名称>interfere22</名称>
/// <功能>计算本AGV路径点与其它AGV机身和路径点干涉</功能>
/// <参数 name = "Car * pCar">指定AGV</参数>
/// <返回>int 1 干涉，0 不干涉, 3 有干涉点，未被锁定</返回>
/// <说明>本AGV路径后面附近点，与其它AGV机身干涉，避让
///       与其它AGV路径点干涉，如果已被锁定，避让，没有锁定，继续判断
/// </说明>
// ?? 是不是可以合并到24中
int Ground::interfere22(Car * pCar)
{
	int nResult = 0;
	INT_PTR j = pCar->m_nRealIndex;

	// ?? 这段程序在本线程中执行，不用锁
	// HLock hl(&m_mutexPath, 1);	// 等待读，退出时自动解锁

	//bool bTurned = false;			// 计算节点干涉标志
	for (INT_PTR length = pCar->m_arrPath.GetCount(); j < length; j++)
	{
		// ?? 本线程中计算，可能不用同步
		PathNode & pn = pCar->m_arrPath[j];		// pCar的路径不用锁定，因为是顺序执行
		int nDis = pCar->calDistance(&pn);

		// 比较所有运动队列中的AGV，AGV区域干涉情况。
		INT_PTR nCars = m_arrCars.GetCount();
		for (INT_PTR i = 0; i < nCars; i++)
		{
			Car * pc = m_arrCars[i];

			if (pc->getDisable())	// 停用
				continue;
			if (pc == pCar)	// 排除自己
				continue;

			ASSERT(length >= 2);	// 20201026 加上=号，因为在本段取货是2

			// 20180511 改用AGV实际占用区域判断，两个AGV车体干涉，立即停止
			ASSERT(pc->m_pGeoAGV);
			if (/*!bTurned &&*/ nDis < pCar->m_nAvoidDistance)
			{
				UINT nr = pn.compareCarExt(pc, pc->m_pGeoEXT);
				if (GETWORD(nr, 1) != 1													// 如果转弯区被占，避让，"E:\GIT\调度指挥\避让问题5.docx" 2024年4月30日
					|| (GETWORD(nr, 0) != 1												// 锁定区干涉
						&& !(pc->m_returnWcs.bAvoid)						// 对方避让时不避让，通过
						)
					)
				{
					pCar->m_pAvoidCar = pc;
					pCar->m_nAvoidIndex = j;
					return 1;
				}

				//bTurned = pn.m_bTurnPrevious;
			}
			//else
			//	bTurned = true;

			HLock hl(&pc->m_mutexPath, 1, 4);	// 等待读，退出时自动解锁
			if (!hl.allowed())			// 路径被别处锁定时跳过
				continue;

			// 与其它AGV锁定的点是否有干涉
			INT_PTR lenPath2 = pc->m_arrPath.GetCount();		// 最后一点后面单独算
			for (INT_PTR k = 0; k < lenPath2; k++)
			{
				// 中间点提前避让
				if (k < pc->m_nRealIndex							// 进入中间点
					//&& !pc->m_arrPath[k].m_pLockGeometry		// 已过的点没有锁定	// 20220805 把这个条件移到下面
					)
				{
				/*
					// 20220409 端点有AGV，在中点外等待
					// 有正在驶入的AGV
					if(((pc->m_pEndPoint
						&& pc->m_pEndPoint->m_nMiddleCard == pc->m_arrPath[k].m_nCard			// k是中间点
						&& pc->m_pEndPoint->m_nMiddleCard == pn.m_nCard							// 正在测试的点是中间点
						)
						//|| lenPath2 <= 3	// ?? 20220805 南基临时增加这个条件，解决AGV在中间点里面的问题，// ?? 20220826 可能有三段
						)
						&& pCar->m_pEndPoint
						&& pCar->m_pEndPoint->m_nMiddleCard == pc->m_pEndPoint->m_nMiddleCard	// 两台AGV的中间点相同
						&& pCar->m_pEndPoint->m_nMiddleCard == pn.m_nCard						// 正在测试的点是中间点
						)
					{
						int dd = pCar->calDistance2(&pn);
						if (dd < theApp.m_nExtSafe)										// ?? 多加一些距离，防止靠外边的道路双避让，加了下面的条件，解决这个问题
						{
							// 20220826 增加条件目标点（不一定是对方AGV的中间点），距离小于避让距离
							int d = pCar->m_pReadData->nDistance;
							//if (d < 0)
							//	d = -d;
							//if (d < pCar->m_nAvoidDistance)
							{
								pCar->m_pAvoidCar = pc;
								pCar->m_nAvoidIndex = j;
								return 1;	// 有相同点，说明在区域里面
							}
						}
					}
					*/

					if (!pc->m_arrPath[k].m_pLockGeometry		// 已过的点没有锁定	// 20220805 把这个条件移到这里
						)
						continue;	// 已经过的点，没有锁定，跳过
				}

				PathNode & pn2 = pc->m_arrPath[k];

				// 20210626 修改bug，i是Car索引，改为j，如果相同，跳出，并比较下一个pc
				//if (isSameDirection(pCar->m_arrPath, i, pc->m_arrPath, k))
				//	continue;	// 20180129 同向同路不算
				if (isSameDirection(pCar->m_arrPath, j, pc->m_arrPath, k))
					break;

				// 20190313 重要改变 这一段移到上两层循环
				//int dis2 = theApp.m_nSafeDis * 5 / 4;
				//if (pCar->calDistance(&pn2) > dis)	// theApp.m_nExtSafe) 	// 20190311改为安全距离，详见OneNote/GHCY/2019/2019/3/11 14:22
				//	continue;	// 本AGV大于安全距离，跳过

				//if (pc->calDistance(&pn2) > dis2)	// 20190311改为安全距离，详见OneNote/GHCY/2019/2019/3/11 14:22
				//	continue;

				if (pn2.m_nCard == pCar->m_arrPath[(INT_PTR)(pCar->m_nRunIndex) - 1].m_nCard)
					continue;	// 20190214 刚过的点不算，详见OneNote/GHCY/2019/2019/2/14 11:40

				// 计算交叉
				//D2D1_GEOMETRY_RELATION aa = pGeo->CompareWithGeometry(*pn2.m_pTurnGeometry, D2D1::Matrix3x2F::Identity(), 100.f);	// 1000次 5ms
				// 20181206 从compareLock改为compareNode，用转弯区判断是否相交
				//D2D1_GEOMETRY_RELATION aa = pn2.compareLock(pn);	// 1000次 5ms
				D2D1_GEOMETRY_RELATION aa = pn2.compareNode(pn);	// 1000次 5ms
				if (aa != 1)	// 与锁定点有干涉
				{
					// 20180607加条件，对方是避让就不避让，转弯时占用的范围较大有干涉也强行通过
					// ?? 强行通过可能有问题，虽然有机身判断，但是机身位置可能不准
					// 20200416 增加条件 pc->isTurning()，否则江西唯美6号线打带时会卡8号线
					// 20200930 isTurning() 改成 
					//if (pn2.m_pLockGeometry && !(pc->m_pReadData->nCmd & 0x8000) && pc->isTurning())
					if (pn2.m_pLockGeometry
						&& !pc->m_returnWcs.bAvoid
						//&& pc->m_pReadData->nState == 0x24
						//&& bAvoidDis								// 20220810 加这个条件
						)
					{
						pCar->m_pAvoidCar = pc;
						pCar->m_nAvoidIndex = j;
						return 1;
					}
					//else if (interfere23(pCar, j, pc, k, 3))	// 判断逆向干涉
					//{
					//	pCar->m_pAvoidCar = pc;
					//	pCar->m_nAvoidIndex = j;
					//	return 1;
					//}
					//else if(bAvoidDis)						// 20220810 加这个条件，减小避让距离
						nResult = 3;	// 有交叉，未锁定，待后续处理
				}

				// 导航到第一段的终点，对方有AGV，计算避让
				if (j == length - 1			// 终点
					&& !pCar->m_pEndPoint	// 不是端点
					&& pn.m_nCard == pn2.m_nCard
					)
				{
					nResult = 3;	// 第一段导航的终点有其它AGV，继续计算
				}

				// 20190315 改为扩展安全距离，详见OneNote/GHCY/2019/2019/3/11 14:22
				// 20190317 AGV很远时可能逆向干涉，此法不适用，注释，详见OneNote/GHCY/2019/2019/3/17 14:37
				//if (pc->calDistance(&pn2) > theApp.m_nExtSafe)
				//	break;
			}
		}
	}

	ASSERT(nResult != 1);
	return nResult;
}


/// <作者>Mr. Hu</作者>
/// <日期>20180616</日期>
/// <名称>interfere23</名称>
/// <功能>判断逆向干涉</功能>
/// <参数 name = "Car * pCar1">指定的AGV</参数>
/// <参数 name = "int nIndex1">pCar1的路径索引</参数>
/// <参数 name = "Car * pCar2">另一台AGV</参数>
/// <参数 name = "int nIndex2">pCar2的路径索引</参数>
/// <参数 name = "int nInterType">起始类型，2 机身干涉，3 路径点干涉</参数>
/// <返回>int 0 继续，1 避让</返回>
/// <说明>判断pCar2是不是逆向AGV，如果是并且已经占道，避让，否则排除此AGV，以后的程序跳过之，同时排
///		  除同向AGV
/// </说明>
int Ground::interfere23(Car * pCar1, INT_PTR nIndex1, Car * pCar2, INT_PTR nIndex2, int nInterType)
{
	// ?? 要不要锁
	HLock hl(&pCar1->m_mutexPath, 1, 5);	// 等待读，退出时自动解锁
	if (!hl.allowed())			// 路径被别处锁定时跳过
		return 0;
	HLock hl2(&pCar2->m_mutexPath, 1, 6);	// 等待读，退出时自动解锁
	if (!hl2.allowed())			// 路径被别处锁定时跳过
		return 0;

	CArray<PathNode, PathNode&> & path1 = pCar1->getPath();
	INT_PTR len1 = path1.GetCount();
	CArray<PathNode, PathNode&> & path2 = pCar2->getPath();
	INT_PTR j = nIndex2;
	INT_PTR nRunIndex2 = (INT_PTR)(pCar2->m_nRealIndex) - 1;
	if (nRunIndex2 < 0)
		nRunIndex2 = 0;
	INT_PTR i = nIndex1;
	int nDis = 0;				// 路径点干涉
	int nSafe = (int)pCar1->m_fGeoLength + 2600;
	BOOL bPrevious = FALSE;		// 上一个节点重合
	BOOL bReverse = FALSE;		// 逆向AGV

	PathNode* ppnReverse = 0;

	ASSERT(nRunIndex2 >= 0 && nRunIndex2 < path2.GetCount());

	// 从本AGV起始点开始往后计算
	for (; 
		(i < len1)
		&& (j >= 0)	// 20220405 增加这个条件，在各线私有点发车时，nn == nRunIndex2 == 0，后面的计算 j = -1
		;			// 20220605 j >= 0，避免最后一段不计算，《E:\AGV记录\避让问题\避让问题4.docx/2022年6月5日星期日 11：19》
		i++)
	{
		PathNode & pn1 = path1[i];
		//PathNode & pn2 = path2[j];
		BOOL bCurrent = FALSE;	// 为TRUE时当前节点重合
		Car* pCarAvoid = 0;		// 预避让

		// 用区域判断有没有干涉
		INT_PTR nn = nRunIndex2;
		//if (pCar2->isTurning())		// 如果是转弯，计算到转弯点，转弯完成后发下一条数据
		//	nn++;
		for (INT_PTR jj = j;
			(jj >= nn || (jj >= 0 && path2[jj].m_pLockGeometry))
			&& jj > 0	// 20220405 增加这个条件，在各线私有点发车时，nn == nRunIndex2 == 0，后面的计算 j = -1
			;
			jj--)
		{
			PathNode& pn2 = path2[jj];
			//int len = pn1.calDis(pn2);
			int nDis2 = pCar1->calDistance(&pn1);
			if ((pn2.m_bTurnProgram || pn1.m_bTurnProgram)	// AGV是转弯，按转弯面积计算
				&& pn1.m_pTurnGeometry
				&& pn2.m_pTurnGeometry
				//&& pn1.calDis(pn2) < theApp.m_nSafeDis * 3		// 在一定距离内
				&& pn2.m_pLockGeometry								// 被对方锁定		// ?? 不锁定的可以也要判断
				&& nDis2 < pCar1->m_nAvoidDistance								// 被对方锁定		// ?? 不锁定的可以也要判断
				)
			{
				// 判断交集
				//D2D1_GEOMETRY_RELATION aa = pn2.compareNode(pn1);	// 1000次 5ms
				D2D1_GEOMETRY_RELATION aa = pn2.compareLock(pn1);	// 1000次 5ms
				if (aa != D2D1_GEOMETRY_RELATION_DISJOINT
					&& !pCar2->m_returnWcs.bAvoid//(pCar2->m_pReadData->sd.sd9.nCmd & 0x8000)
					)
				{
					pCarAvoid = pCar2;	// 逆向AGV锁定节点，避让
					//j = jj;	// 节点不重合，区域相交，重新计算可用距离
					break;
				}
			}
		}

		// 不重合，判断区域是否干涉
		//D2D1_GEOMETRY_RELATION aa = D2D1_GEOMETRY_RELATION_DISJOINT;
		if (pn1.m_nCard != path2[j].m_nCard)		// 不同路
		{
			/*if (path2[j].m_nTurnAngle	// 如果其它AGV是转弯，按转弯面积计算
				&& pn1.m_pTurnGeometry
				&& path2[j].m_pTurnGeometry
				&& pn1.calDis(path2[j]) < theApp.m_nSafeDis * 2
				)
			{
				// 判断交集
				aa = path2[j].m_pTurnGeometry->CompareNode(*pn1.m_pTurnGeometry, m_mtxIdentity, 100.f);	// 1000次 5ms
				if (aa != D2D1_GEOMETRY_RELATION_DISJOINT)
					bCurrent = TRUE;	// 节点不重合，区域相交，重新计算可用距离
			}

			// 区域不相交，看前面的点是否相交
			if (aa == D2D1_GEOMETRY_RELATION_DISJOINT)*/
			//{
			//	for (INT_PTR jj = j; jj > nRunIndex2 || path2[jj].m_bLocking; jj--)
			//	{
			//		if (pn1.m_nCard == path2[jj].m_nCard)
			//		{
			//			j = jj;
			//			//pn2 = path2[j];
			//			break;
			//		}
			//	}
			//}

			// 20240923 "E:\GIT\调度指挥\避让问题5.docx"2024年9月20日星期五 6:45
			UINT nr = pn1.compareCarExt(pCar2, pCar2->m_pGeoEXT2);
			if (GETWORD(nr, 0) != 0x0001)
			{
				pCarAvoid = pCar2;	// 逆向的AGV已经占路，避让
			}










			//INT_PTR nn = nRunIndex2;
			//if (pCar2->isTurning())		// 如果是转弯，计算到转弯点，转弯完成后发下一条数据
			//	nn++;
			//for (INT_PTR jj = j; jj >= nn || (jj >= 0 && path2[jj].m_pLockGeometry); jj--)
			for (INT_PTR jj = j;
				(jj >= nn || (jj >= 0 && path2[jj].m_pLockGeometry))
				&& jj > 0	// 20220405 增加这个条件，在各线私有点发车时，nn == nRunIndex2 == 0，后面的计算 j = -1
				;
				jj--)
			{
				if (pn1.m_nCard == path2[jj].m_nCard)
				{
					j = jj;
					break;
				}
			}
		}

		// 上一段程序里面改变了pn2和j，原来不等也可能就成相等
		if (pn1.m_nCard == path2[j].m_nCard)		// 不同路
		{
			// 排除同路同向
			if (EQUAL(pn1.m_fRoadRad, path2[j].m_fRoadRad, 0.001)		// 同向，不含第一个重合点
				//&& !pn2.m_bLocking					// 未被锁
				)	// 同向的不避让
			{
				//pCar2->m_bCancelInter = TRUE;		// 同向AGV排除避让
				return 0;		// 如果是同向AGV跳出，//?? 转了一圈又回来逆向行驶没有考虑
			}

			// 20210602 解决AGV在中间时不避让的问题
			// 详见F:\避让问题\避让问题3.docx\2021年6月2日星期三
			//ASSERT(i > 0);

			// 20220803 解决有时 i = 0
			if (!i)
				i++;

			UINT result = pn1.compareCarExt( pCar2, pCar2->m_pGeoEXT);	// 1000次 5ms

			if(bPrevious												// 前一点重合
				&& (result >> 16) != 1
				)
			{
				pCarAvoid = pCar2;	// 避让
			}
			//if(bPrevious												// 前一点重合
			//	&& pn1.m_nCard == pCar2->m_pReadData->nStartCard		// 当前AGV与逆向AGV的已过卡相同
			//	&& pCar2->m_pReadData->nStartCard != pCar2->m_pReadData->nEndCard	// 避免一种特殊情况，详见F:\避让问题\避让问题3.docx\2022年5月26日星期四
			//	)
			else if (bPrevious													// 前一点重合
				&& path1[i - 1].m_nCard == path2[j + 1].m_nCard				// 当前AGV与逆向AGV的已过卡相同
				&& path1[i - 1].m_nCard == path2[pCar2->m_nRealIndex].m_nCard		// 当前AGV与逆向AGV的已过卡相同
				)
			{
				pCarAvoid = pCar2;	// 避让
			}
			else if (path2[j].m_pLockGeometry)			// 锁定
			{
				pCarAvoid = pCar2;	// 逆向AGV锁定节点，避让
			}

			ASSERT(j >= 0);
			if (bCurrent && j <= nRunIndex2)	// ???? 这个判断改用bCurrent和bPrevioes	// 20201029 加上bCurrent，避免另一AGV在当前位置存货时避让
			//if (j >= nRunIndex2)	// ???? 这个判断改用bCurrent和bPrevioes	// 20201029 改为>=，只判断当前位置以后的路
			{
				pCarAvoid = pCar2;	// 逆向的AGV已经占路，避让
			}

			// 导航到第一段的终点，对方有AGV，计算避让
			// 20220416 注释这一段，因为南基线路有一小段逆行，会等很远的AGV，// 20220419问题太多，又使用
			// 20220805 这一段可能是控制两台AGV不能同时进最后一段，现在可以不用了，注释
			//if (i == len1 - 1			// 终点
			//	// && !pCar1->m_pEndPoint	// 不是端点			// 20220315 凤铝，取消这一段，最后一段也计算
			//	&& ((j - pCar2->m_nRunIndex) <= 2)				// 20220315 如果本AGV到终点，另一AGV还有2段以上，逆向不避让。
			//	)
			//{
			//	pCarAvoid = pCar2;	// 逆向AGV锁定节点，避让
			//}

			if (!ppnReverse)
				ppnReverse = &path2[j];

			bCurrent = TRUE;

			j--;
		}
		else
			ppnReverse = 0;



		if (bPrevious && bCurrent)	// 有交点，可用距离清零
		{
			//nDis = 0;			// 上一个节点和本节点都相同，复位可能距离
			bReverse = TRUE;	// 至少有一段路重合，反向，表
		}
		else if (i == nIndex1)
		{
			if (nInterType == 2)
			{
				//nDis = pCar1->calDistance(&pn1) - theApp.m_nSafeDis;	// 取初始距离，重合点后面的距离
				nDis = pCar1->calDistance(&pn1);	// 取初始距离，重合点后面的距离
				if (nDis < 0)
					nDis = 0;
			}
			else
				nDis = 0;

			//ppnReverse = 0;
		}
		else
		{
			ASSERT(i > 0);
			//if (path1[i - 1].lageAngle())		// 如果上一节点是转弯减去一些距离
			//{
			//	nDis -= theApp.m_nHalfSafe / 2;
			//	if (nDis < 0)
			//		nDis = 0;
			//}
			nDis += pn1.m_nLength;				// 第二次以后，没有重合路段，累加

			//ppnReverse = 0;
		}

		bPrevious = bCurrent;

		// 把m_nExtSafe改成m_nSafeDis, 解决《避让问题2.docx 中 2019年12月18日星期三》 问题
		if (nDis > nSafe)
		{
			//continue;
			break;
		}

		if (pCarAvoid)
		{
			pCar1->m_pAvoidCar = pCarAvoid;
			if(pCar1->m_nAvoidIndex > i)
				pCar1->m_nAvoidIndex = i;
			return 1;	// 逆向的AGV已经锁定点烟器，避让
		}

		if (bCurrent)	// 有交点，可用距离清零
			nDis = 0;

		//bPrevious = bCurrent;
	}

	// 20240919注释这一段，解决"E:\GIT\调度指挥\避让问题5.docx"2024年9月19日星期四 15:48
	// 判断本AGV防护区与对方AGV转弯区相交
	//if (nDis > nSafe)
	//{
	//	if (ppnReverse)
	//	{
	//		HLock hGeoExt(&afxMutexGeo, 1);	// 等待读，退出时自动解锁

	//		UINT nr = ppnReverse->compareCarExt(pCar1, pCar1->m_pGeoEXT2);
	//		if (nr != (D2D1_GEOMETRY_RELATION_DISJOINT | (D2D1_GEOMETRY_RELATION_DISJOINT << 16)))	// 高低字都是1，无干涉
	//			return 1;	// 有干涉
	//	}
	//}

	return 0;
}

/// <作者>Mr. Hu</作者>
/// <日期>20180526</日期>
/// <名称>interfere24</名称>
/// <功能>综合判断所有AGV</功能>
/// <参数 name = "Car * pCar">指定的AGV</参数>
/// <参数 name = "int nInterType">起始类型，2 机身干涉，3 路径点干涉</参数>
/// <返回>int 0 继续，1 避让</返回>
/// <说明>指定AGV在安全距离内与其它AGV交叉，但未被锁定，继续拄前试运行，看能不能穿过去，这时同向和逆向的都被排除</说明>
int Ground::interfere24(Car * pCar, int nInterType)
{
	ASSERT(nInterType == 2 || nInterType == 3);
	CArray<PathNode, PathNode&> & path1 = pCar->getPath();
	INT_PTR nCars = m_arrCars.GetCount();
	int nDis = 0;	// 可用距离
	int nSafe = (int)pCar->m_fGeoLength + 2600;
	// 为解决"E:\GIT\调度指挥\避让问题5.docx 2024年5月2日星期四" 不减1
	INT_PTR nOrigin = (INT_PTR)pCar->m_nRealIndex;
	//INT_PTR nOrigin = (INT_PTR)pCar->m_nRunIndex - 1;			// 20240412 从上一个开始算，因为占美6号可能从近距离时跳到下一点
	//if (nOrigin < 0)
	//	nOrigin = 0;

	//if (!pCar->isTurning())
	//	nOrigin++;	// 不是转弯算上一点，转弯算当前点，解决 OneNote/GHCY/2019/2019/8/15 20:14 出现的问题

	// 20220606 如果上一点在AGV区域中，从上一点开始算，解决《E:\AGV记录\避让问题\避让问题4.docx/2022年6月6日星期一 1418》
	if (nOrigin > 0)
	{
		//PathNode& pn1 = path1[nOrigin];
		//if(pn1.m_nLength < -pCar->m_pReadData->nDistance)
		//	nOrigin--;												// 以上一点为基准
		//else
		{
			PathNode& pn5 = path1[nOrigin - 1];
			UINT aa5 = pn5.compareCar(pCar, pCar->m_mtxCar);			// 上一点与AGV区域比较
			if (GETWORD(aa5, 0) != 1)									// 节点区有干涉说明AGV覆盖
				nOrigin--;												// 以上一点为基准
		}
	}

	for (INT_PTR i = nOrigin, length = path1.GetCount(); i < length; ++i)
	{
		PathNode & pn1 = path1[i];



		// 锁定卡号，防止多AGV同时计算这个卡。
		//NodeColl* pnc = theApp.m_pGround->m_nodesColl.m_pArrCards[pn1.m_nCard];
		//HLock hlCard(&pnc->m_mutexCard, -1, -1);	// 等待写，退出时自动解锁




		BOOL bCurrent = FALSE;	// 为TRUE时当前节点重合
		Car* pCarAvoid = 0;		// 预避让

		for (INT_PTR j = 0; j < nCars; j++)
		{
			Car * pc = m_arrCars[j];
			if (pc->getDisable())	// 停用
				continue;
			if (pc == pCar)			// 排除自己
				continue;
			//if (!pc->m_nRunIndex)
			//	continue;			// 没有初始化，跳过
			if (!pc->m_arrPath.GetCount())
				continue;


			// 20220712 解决《E:\AGV记录\避让问题\避让问题4.docx/2022年7月11日星期一 1422》问题，共两处
			// 20220716 把2倍改为3倍，解决《E:\AGV记录\避让问题\避让问题4.docx/2022年7月16日星期六 0716》问题，共两处
			// 20220809 ?? 注释，南基临时解决《E:\AGV记录\避让问题\避让问题4.docx/2022年8月9日星期二 0628》问题，共两处
			//int d = pc->calDistance(&pn1);
			//if (d > 3 * theApp.m_nSafeDis)		// 2倍安全距离外的不计算
			//	continue;

			//if (pc->m_bCancelInter)	// 排除同逆向	// 20180726 重新定规则，因为同向时，对方正在避让，本AGV跟过去可能挡住别的AGV，卡住
			//	continue;

			HLock hl(&pc->m_mutexPath, 1, 7);	// 等待读，退出时自动解锁
			if (!hl.allowed())			// 路径被别处锁定时跳过
				continue;



			//// 加上大括号，能锁就放开，对方可锁定；锁不住就等待对方计算完毕；
			//{
			//	HLock hlCard(&pCar->m_nutexAvoid, -1, -1);	// 等待写，退出时自动解锁
			//}
			//HLock hlCard(&pc->m_nutexAvoid, -1, -1);	// 写锁定，pc不进行避让计算





			// 20181207加上按AGV车身判断解决《C:\HW\AGV记录\测试记录\避让.docx\2.AGV20走到附近才不能转弯，卡住》
			// 20220505 南基注释这一段
			// 20240411 增加按指定距离判断，提高准确性
			// 注释这段代码，解决"E:\GIT\调度指挥\避让问题5.docx 2024年5月2日星期四 14:27" 问题
			/*
			D2D1::Matrix3x2F mtx = pn1.m_mtxRegion;
			//PathNode& pn0 = path1[i - 1];
			if( nDis + pn1.m_nLength > nSafe			// 大于指定距离
				&& i > 0								// 不是第一点
				&& path1[i - 1].m_nRoad == pn1.m_nRoad	// 同路
				)
			{
				D2D1::Matrix3x2F mtx0 = path1[i - 1].m_mtxRegion;
				float fs = sqrt((mtx.dx - mtx0.dx) * (mtx.dx - mtx0.dx)
					+ (mtx.dy - mtx0.dy) * (mtx.dy - mtx0.dy));
				float fl = nDis + pn1.m_nLength - nSafe;
				float fc = fl / fs;
				mtx.dx = mtx0.dx + fc * (mtx.dx - mtx0.dx);
				mtx.dy = mtx0.dy + fc * (mtx.dy - mtx0.dy);
			}
			
			bool bAvoid = false;
			UINT nr = pn1.compareCar2(pc, mtx);
			if (GETWORD(nr, 0) != 1)
				bAvoid = true;								// 节点被对方锁定，避让
			else if (GETWORD(nr, 1) != 1)					// 节点未锁定，继续判断
			{
				HLock hGeoExt(&afxMutexGeo, 1);				// 等待读，退出时自动解锁
				nr = pCar->compareCar(pc);					// 与对方AGV比较
				if(GETWORD(nr, 1) != 1)						// 停止区干涉，避让
					bAvoid = true;
				else if (GETWORD(nr, 0) != 1
					&& !(pc->m_pReadData->sd.sd9.nCmd & 0x8000)
					)
					bAvoid = true;	
			}

			if (bAvoid)
			{
				if (pCar->m_nAvoidIndex > i)
					pCar->m_nAvoidIndex = i;	// 避让点

				pCarAvoid = pc;		// 短距离交叉后AGV转向与其它AGV机身干涉，避让
				bCurrent = TRUE;	// 为TRUE时当前节点重合

				break;
			}
			*/

			// 计算路径干涉
			CArray<PathNode, PathNode&> & path2 = pc->getPath();
			//INT_PTR kk = (INT_PTR)pc->m_nRunIndex;
			INT_PTR kk = (INT_PTR)pc->m_nRealIndex;
			INT_PTR k = 0;		// 20180726 已经过的点可能被锁定
			for (INT_PTR length2 = path2.GetCount() - 1; k < length2 /*&& !pc->m_bCancelInter*/; k++)	// 第1点和最后一点不判断，非同向
			{
				PathNode & pn2 = path2[k];

				if (!pn2.m_pLockGeometry && k < kk)
					continue;

				// 20220710 锁定卡号，防止多AGV同时计算这个卡。大括号表示立即解锁，避免死锁
				if (pn1.m_nCard > 0)							// 只有大于0的卡号在集合中
				{
					NodeColl* pnc1 = m_colls.m_nodesColl.m_pArrCards[pn1.m_nCard];
					HLock hlCard1(&pnc1->m_mutexCard, -1, DWORD (-1));	// 等待写，退出时自动解锁
				}
				// 锁定卡号，防止多AGV同时计算这个卡。
				HLock hlCard2;									// 先建一个空锁，确定pn2.m_nCard大于零再锁定
				if (pn2.m_nCard > 0)							// 只有大于0的卡号在集合中
				{
					NodeColl* pnc2 = m_colls.m_nodesColl.m_pArrCards[pn2.m_nCard];
					hlCard2.init(&pnc2->m_mutexCard, -1, DWORD (-1));	// 等待读，退出时自动解锁
				}

				//1111111111111111111111111111111111111111111111111111111111111
				//if (pc->calDistance(&pn2) > 3 * theApp.m_nSafeDis)
				//	continue;	// 20190212 对方AGV大于安全距离，跳过，详见OneNote/GHCY/2019/2019/2/13 10:45	// 改为3倍，详见OneNote/GHCY/2019/2019/3/5 10:11

				// 有干涉
				bool bInterNode = false;							// 节点干涉
				bool bInterCar = false;								// AGV扩展区与节点转弯区干涉
				if (pn1.m_nCard != pn2.m_nCard)						// 不同节点
				{
					if (!pn1.m_bTurnProgram && !pn2.m_bTurnProgram)	// 如果不是转弯，跳过
						continue;

					if (pn1.calDis(pn2) > theApp.m_nSafeDis			// 如果是转弯，并且在指定距离内，按转弯面积计算
						|| !pn1.m_pTurnGeometry
						|| !pn2.m_pTurnGeometry)
					{
						continue;
					}

					// 判断交集
					////D2D1_GEOMETRY_RELATION aa = pn2.m_pTurnGeometry->CompareWithGeometry(*pn1.m_pTurnGeometry, D2D1::IdentityMatrix(), 100.f);	// 1000次 5ms
					//D2D1_GEOMETRY_RELATION aa1 = pn2.compareLock(pn1);	// 1000次 5ms
					//if (aa1 == 1)
					//	continue;	// 无干涉，继续

					// 先判断两个路径节点区域
					//D2D1_GEOMETRY_RELATION aa1 = pn2.compareLock(pn1);	// 1000次 5ms
					D2D1_GEOMETRY_RELATION aa1 = pn1.compareLock(pn2);	// 1000次 5ms			// 20220606 改变主次关系，《E:\AGV记录\避让问题\避让问题4.docx/2022年6月6日星期一 1616》
					bInterNode = (aa1 != 1);
					// 20220606 加上扩展区与转弯区判断，解决《E:\AGV记录\避让问题\避让问题4.docx/2022年6月5日星期日 0820》
					if (!bInterNode)	// 两个AGV的路径点无干涉
					{
						if (i == nOrigin)
						{
							UINT aa2 = pn2.compareCar(pCar, pCar->m_mtxExt);	// 1000次 5ms
							bInterCar = (GETWORD(aa2, 1) != 1);
							if (!bInterCar)									// 高字节是对方AGV路径节点转弯区与本AGV扩展区干涉情况
								continue;									// 如果没有干涉，跳过
							//else
							//	aa2 = aa2;
						}
						else
							continue;
					}
				}
				else
				{
					if (EQUAL(pn1.m_fRoadRad, pn2.m_fRoadRad, 0.001))		// 同向
					{
						//pc->m_bCancelInter = TRUE;	// 同向标志
						//continue;	// 只判断有没有锁定点

						// 20220714 同向时直接跳出，解决《E:\AGV记录\避让问题\避让问题4.docx/2022年7月11日星期一 1422》问题
						break;
					}

					bCurrent = TRUE;	// 节点重合，为TRUE时当前节点重合或有交集

					if (pCar->m_nAvoidIndex > i)
						pCar->m_nAvoidIndex = i;	// 避让点
				}

				// 被对方锁定，避让
				if (pn2.m_pLockGeometry					// 锁定
					&& pn1.m_nCard == pn2.m_nCard		// 20220505 南基加一个条件，不是同一点不算
					)
				{
					pCarAvoid = pc;	// 逆向AGV锁定节点，避让
				}

				// 导航到第一段的终点，对方有AGV，计算避让
				if (i == length - 1			// 终点
					&& !pCar->m_pEndPoint	// 不是端点
					&& pn1.m_nCard == pn2.m_nCard
					)
				{
					pCarAvoid = pc;	// 逆向AGV锁定节点，避让
				}

				// 判断远距离逆向干涉，中间可能有小段分开
				int nType = i == nOrigin ? nInterType : 3;
				if (interfere23(pCar, i, pc, k, nType))	// 判断逆向干涉
				{
					pCarAvoid = pc;	// 逆向AGV避让
				}
				else
					break;
			}

			// 同向处理
			/*if (pc->m_bCancelInter)	// 如果是同向，只判断安全距离内有没有被锁定
			{
				BOOL bs = FALSE;	// 重合点
				INT_PTR k2 = 0;		// 20180726 已经过的点可能被锁定
				for (INT_PTR length2 = path2.GetCount() - 1; k2 < length2; k2++)	// 第1点和最后一点不判断，非同向
				{
					PathNode & pn2 = path2[k2];

					// 相同点
					if (pn1.m_nCard == pn2.m_nCard)		// 不同节点
						bs = TRUE;

					if (bs)	// 从重合点往后算距离，如果有锁定，避让
					{
						int dd = pn1.calDis(pn2);	// ?? 直线距离，复杂路径可能不准
						if (dd > theApp.m_nSafeDis)	// 同向AGV大于安全距离没有锁定，跳出
							break;

						if (pn2.m_pLockGeometry)
						{
							pCarAvoid = pc;	// 同向AGV锁定节点，避让
							break;
						}
					}
				}
			}*/
		}

		if (i == nOrigin)	// 起始路段
		{
			if (nInterType == 2					// 机身干涉
				|| nInterType == 3				// 20220809 南基，加这个条件
				)
			{
				//nDis = pCar->calDistance(&pn1) - theApp.m_nHalfSafe;	// 取初始距离减去车长，类似半安全距离。
				nDis = pCar->calDistance(&pn1);	// 取初始距离减去车长，类似半安全距离。
				if (nDis < 0)
					nDis = 0;
			}
			else
			{
				nDis = 0;						// 路径点干涉，取0
				//nSafe = theApp.m_nExtSafe;		// 20200423 解决《避让问题2.docx/20200421》
			}
		}
		else	// 累加可用距离
		{
			nDis += pn1.m_nLength;		// 第二次以后，没有重合路段，累加
		}

		if (pCarAvoid)
		{
			pCar->m_pAvoidCar = pCarAvoid;
			return 1;	// 逆向的AGV已经锁定节点，避让
		}

		// 20240627 解决"E:\GIT\调度指挥\避让问题5.docx"，从“if (pCarAvoid)”上面移到这里
		if (nDis > nSafe)
			return 0;	// 有足够的可用距离，继续运行

		if (bCurrent)
		{
			nDis = 0;
			// 第二次以后，用扩展安全距离
			// 20190130 改为只用基本安全距离，转弯减1000mm，
			// 不行，三车循环时过不去，如C:\Users\hcl-w530\Desktop\Works\AGV记录\避让问题\避让问题2.docx中2019年1月31日20:30
			// 把m_nExtSafe改成m_nSafeDis, 解决《避让问题2.docx 中 2019年12月18日星期三》 问题，上一行问题另外想算法

			//nSafe = theApp.m_nExtSafe;
		}

		//if (pn1.lageAngle())
		//{
		//	nDis -= 1000;	// 20190130 如果是转弯减1000mm
		//	if (nDis < 0)
		//		nDis = 0;
		//}
	}

	return 0;
}

/// <作者>Mr. Hu</作者>
/// <日期>20201117</日期>
/// <名称>interfere25</名称>
/// <功能>计算区域是否允许进车</功能>
/// <参数 name = "Car * pCar">指定AGV</参数>
/// <返回>int 1 干涉，0 不干涉</返回>
/// <说明>如果区域只能进一辆车，并且有车，避让
/// </说明>
int Ground::interfere25(Car* pCar)
{
	EndPoint* pep0 = pCar->m_pEndPoint;
	if (!pep0)
		return 0;

	if (!pep0->m_nMiddleCard)	// 没有中间点，返回0，继续
		return 0;

	// ?? 要不要锁
	HLock h0(&pCar->m_mutexPath, 1, DWORD (-5));	// 等待读，退出时自动解锁
	//if (!h0.allowed())			// 路径被别处锁定时跳过
	//	return 0;

	// 判断本AGV是否过中点，如果不过，不避让
	//INT_PTR len0 = pCar->m_arrPath.GetCount();
	//INT_PTR nIndex0 = pCar->m_nRunIndex;
	//for (; nIndex0 < len0; nIndex0++)
	//{
	//	PathNode& pn0 = pCar->m_arrPath[nIndex0];		// pCar的路径不用锁定，因为是顺序执行
	//	if (pn0.m_nCard == pep0->m_nMiddleCard)	// 不是中间点
	//		break;
	//}
	//if (nIndex0 == len0)
	//	return 0;

	// 20220327 南基，暂时取消这个条件
	//if (-pCar->m_nDisDest > pep0->m_nMiddleDis ) // 没有到指定距离
	//	return 0;

	// 比较所有运动队列中的AGV，AGV区域干涉情况。
	INT_PTR nCars = m_arrCars.GetCount();
	for (INT_PTR i = 0; i < nCars; i++)
	{
		Car* pc = m_arrCars[i];

		if (pc->getDisable())	// 停用
			continue;
	
		if (pc == pCar)	// 排除自己
			continue;
		
		//if (!pc->m_nRunIndex)
		//	continue;			// 没有初始化，跳过

		//if (pc->m_bCancelInter)	// 排除同逆向	// 20180726 重新定规则，因为同向时，对方正在避让，本AGV跟过去可能挡住别的AGV，卡住
		//	continue;

		HLock h2(&pc->m_mutexPath, 1, 7);	// 等待读，退出时自动解锁
		if (!h2.allowed())			// 路径被别处锁定时跳过，// ?? 可能错过要避让的车
			continue;


		// 计算路径干涉
		//CArray<PathNode, PathNode&>& path2 = pc->getPath();
		//INT_PTR len2 = path2.GetCount();
		// 只有两段时，存取货完成后在端点转换
		//if (len2 == 2)	// 过渡期
		//{
		//	ASSERT(pc->m_pEndPoint);

		//	//int pid = path2[1].m_nPointID;
		//	//ASSERT(pid);

		// 本AGV过中点
		//	//EndPoint* pep2 = m_arrEndPoint[(INT_PTR)pid - 1];
		//	if (pc->m_pEndPoint->m_nMiddleCard == pep0->m_nMiddleCard)
		//	{
		//		pCar->m_pAvoidCar = pc;
		//		return 1;	// pc 在指定的区内等待，pCar 避让
		//	}
		//	else
		//		return 0;	// 不在对应区
		//}

		//// 判断库区内是否有AGV
		////ASSERT(len2 > 2);
		//INT_PTR kk = 0;
		//INT_PTR k = 0;		// 20180726 已经过的点可能被锁定

		//// 求起点端点
		//int pid2 = path2[0].m_nPointID;
		////ASSERT(pid2);	// 发车时pid2为0

		// 不是正在驶入的AGV
		if (!pc->m_pEndPoint || (pc->m_pEndPoint->m_nMiddleCard != pep0->m_nMiddleCard))
			continue;
		if (!pc->pastMiddle())		// 未过中点
			continue;

		// 如果pc正在驶入，判断已过的点重合则避让
		// 如果pc在在驶出，判断未过的点重合则避让
		int nSafe = pc->m_pEndPoint->m_nMiddleDis;
		//for (; k < kk /*&& !pc->m_bCancelInter*/; k++)	// 第1点和最后一点不判断，非同向
		//{
		//	PathNode& pn2 = path2[k];

		//	for (INT_PTR j = nIndex0; j < len0; j++)
		//	{
		//		if (pCar->m_arrPath[j].m_nCard == pn2.m_nCard)
		//		{
		//			int dd = pCar->calDistance(&pn2);
		//			if (dd < nSafe)
		//			{
		//				pCar->m_pAvoidCar = pc;
		//				return 1;	// 有相同点，说明在区域里面
		//			}
		//		}
		//	}
		//}

		INT_PTR len0 = pCar->m_arrPath.GetCount();
		INT_PTR nIndex0 = pCar->m_nRunIndex;
		for (INT_PTR j = nIndex0; j < len0; j++)
		{
			PathNode& pn2 = pCar->m_arrPath[j];
			if (pn2.m_nCard == pep0->m_nMiddleCard)
			{
				int dd = pCar->calDistance(&pn2);
				if (dd < nSafe)
				{
					pCar->m_pAvoidCar = pc;
					return 1;	// 有相同点，说明在区域里面
				}
			}
		}
	}

	return 0;
}


// 计算道路干涉，LPARAM Car * this
//void Ground::onCarPos(LPNMHDR pNMHDR, LRESULT *pResult)
//{
//	Car * pCar = (Car *)PHNM(pNMHDR)->lParam;
//	//if( pCar->hasPause() )
//	*pResult = interfere( pCar );	// 计算干涉点
//
//}

// 暂停/行驶
// 所有AGV都是暂停状态，全部行驶，否则全部暂停
afx_msg void Ground::onPauseAll()
{
	TRY
	{
		BOOL pause = TRUE;
		// 如果所有AGV都是暂停pause == TRUE，否则pause == FALSE
		INT_PTR len = m_arrCars.GetCount();
		for( int i = 0; i < len; i++ )
		{
			if( !m_arrCars[i]->isPause() )
			{
				pause = FALSE;
				break;
			}
		}

		if( pause )	// 所有车都是暂停
		{
			for(INT_PTR i = 0, n = m_arrCars.GetCount(); i < n; i++ )
				m_arrCars[i]->pauseGo();
		}
		else
		{
			for(INT_PTR i = 0, n = m_arrCars.GetCount(); i < n; i++ )
				m_arrCars[i]->pause();
		}
	}
	CATCH_ALL( e )
	{
		WCHAR msg[1000];
		e->GetErrorMessage( msg, 1000 );

		// 保存日志
		GException ge;
		ge.m_nCode		= 8002;
		ge.m_strObject	= L"Ground::onPauseAll";
		ge.m_strSummary	= msg;
		ge.m_strDetails = L"全部暂停/行驶异常";

		theApp.m_pLogList->addLog( &ge );

		e->ReportError();
	}
	END_CATCH_ALL
}

// 第一次启动后，初始化到充电或等待状态
// 返回0正常，否则出错
/*void Ground::initCar(Car * pCar)
{
	pCar->initCar();

	// 取对应的产品点
	// 20180103 统一到pickup()中
	// 20180104 还是在这里初始化，不用matchProduct( -1 )

	if (pCar->getOutside())
		return;	// 停用时直接返回

	pickup(pCar);
}
*/

// 重新启动后，从PLC恢复数据，PHNM(pNMHDR)->lParam 指向PLC数据，*pResult 返回
// 20181116 移到Car中
/*
void Ground::onCoveryCar(LPNMHDR pNMHDR, LRESULT *pResult)
{
	TRY
	{
		*pResult = 0;

		Car * pCar = (Car *)(PHNM(pNMHDR)->lParam);
		ReadData * prd = pCar->getReadData();
		ASSERT( prd );

		CArray<PathNode, PathNode&> & path = pCar->getPath();
		INT_PTR len = path.GetCount();
		ASSERT(len != 1);	// len == 0 第一次开机，len == 2 在端点等待，len >= 3 导航状态

		if( len == 0 )	// 第一次开机，没有保存路径，先初始化AGV
		{
			initCar( pCar );

			//ASSERT( m_arrCars.GetCount() > m_arrRunCars.GetCount() );	// ?? 目前这两个数相等
			//m_arrRunCars.Add( pCar );

			return;
		}
		else if( (pCar->m_pReadData->nCmd & 0x2000) )	// 离线状态不继续
		{
			return;	// 离线状态不继续
		}
		else if( len == 2 )	// 在端点等待
		{
			// 20170807 改成 - 1 + 1	?? 这个点可能超界
			/*PathNode pn = path.GetAt(0);	// 原来用 PathNode & pn = path.GetAt(0); 后面的代码清除path后，pn的内容全变，慎用!!!!
			int index = pn.m_nID - 1 + 1;	// - 1 + 1，减1变成从0开始的索引，加1变成下一个点(路径起点)。编译器优化这个式子
			if( pn.m_nRoad != m_arrRoadNodes[index].m_nRoad)	// 前一点路号不一样
			{
				GException ge;
				ge.m_nCode		= 2017;
				ge.m_strObject	= pCar->getIPPort();
				ge.m_strSummary	= L"道路节点数据表错误，端点道路只有一点";
				ge.m_strDetails.Format( L"节点 %i", pn.m_nCard );

				theApp.m_pLogList->addLog( &ge );

				*pResult = -17;	//终点道路 %i 只有一点
				return;
			}
			
			int type = getType( &m_arrRoadNodes[index] );
			if( type == 0 )
				//EndPoint * pep = findEndPoint( m_arrRoadNodes[index-1] );
				//if( pep < 0 )	// 异常
			{
				GException ge;
				ge.m_nCode		= 2022;
				ge.m_strObject	= pCar->getIPPort();
				ge.m_strSummary	= L"端点类型错误";
				ge.m_strDetails.Format( L"节点 %i", type );

				theApp.m_pLogList->addLog( &ge );

				*pResult = -22;	//L"找不到AGV所在节点"
				return;
			}

			CarState state = !(type & 0x80) ? !(type & 0x40) ? !(type & 0x20) ? !(type & 0x10)	// 存货，取货，等待，充电
				? CarError : CarRecharged : CarWait : CarGot : CarPut;
			if( state == CarError )	// 异常
			{
				GException ge;
				ge.m_nCode		= 2020;
				ge.m_strObject	= pCar->getIPPort();
				ge.m_strSummary	= L"端点类型错误";
				ge.m_strDetails.Format( L"错误端点 0x%02i", type );

				theApp.m_pLogList->addLog( &ge );

				*pResult = -20;		// 端点类型错误
				return;
			}
			pCar->setFinish( &pn, state );
			* /
			return;


			/*PathNode & pn = path.GetAt(0);
			int index = pn.m_nID - 1;

			if( index == 0 ||	// 没有前一点
				pn.m_nRoad != m_arrRoadNodes[index-1].m_nRoad)	// 前一点路号不一样
			{
				GException ge;
				ge.m_nCode		= 2017;
				ge.m_strObject	= pCar->getIPPort();
				ge.m_strSummary	= L"道路节点数据表错误，端点道路只有一点";
				ge.m_strDetails.Format( L"节点 %i", pn.m_nCard );

				theApp.m_pLogList->addLog( &ge );

				*pResult = -17;	//终点道路 %i 只有一点
				return;
			}

			int type = getType( &m_arrRoadNodes[index-1] );
			if( type == 0 )
				//EndPoint * pep = findEndPoint( m_arrRoadNodes[index-1] );
				//if( pep < 0 )	// 异常
			{
				GException ge;
				ge.m_nCode		= 2022;
				ge.m_strObject	= pCar->getIPPort();
				ge.m_strSummary	= L"端点类型错误";
				ge.m_strDetails.Format( L"节点 %i", type );

				theApp.m_pLogList->addLog( &ge );

				*pResult = -22;	//L"找不到AGV所在节点"
				return;
			}

			CarState state = !(type & 0x80) ? !(type & 0x40) ? !(type & 0x20) ? !(type & 0x10)	// 存货，取货，等待，充电
				? CarError : CarRecharged : CarWait : CarGot : CarPut;
			if( state == CarError )	// 异常
			{
				GException ge;
				ge.m_nCode		= 2020;
				ge.m_strObject	= pCar->getIPPort();
				ge.m_strSummary	= L"端点类型错误";
				ge.m_strDetails.Format( L"错误端点 0x%02i", type );

				theApp.m_pLogList->addLog( &ge );

				*pResult = -20;		// 端点类型错误
				return;
			}
			pCar->setFinish( &pn, state );
			return;* /
		}
		//else if( pCar->m_pReadData->nState == 0x1085 )	// 存货完成，等待数据
		//{
		//	pCar->m_enumState = CarPut;
		//	return;	
		//}
		//else if( pCar->m_pReadData->nState == 0x1045 )	// 取货完成，等待数据
		//{
		//	pCar->m_enumState = CarGot;
		//	return;	
		//}


		// 从下面注释中取回 20170807
		PathNode & pn = path.GetAt( len - 1 );
		if( pn.m_nCard < 0 )	// 是端点
		{
			RoadNode * prn = &m_arrRoadNodes[pn.m_nID-1];
			PathNode & pn0 = path[0];
			ASSERT(pn0.m_nPointID > 0);
			ASSERT(pn.m_nPointID > 0);
			EndPoint * pep2 = m_arrEndPoint[pn.m_nPointID - 1];
			EndPoint * pep0 = m_arrEndPoint[pn0.m_nPointID - 1];
			if (pn.m_nPointID > 0		//?? 数据库路径错误时异常处理，先简单判断
				&& pep0->getCode1() != pep2->getCode1()	// 清远唯美专用，线号相同是到旁边避让，不设终点
				)
			{
				pCar->setEndNode(prn, m_arrEndPoint[pn.m_nPointID - 1]);	// 第一个参数必须从m_arrRoadNodes中取得，不能直接用pn，因为pn有可能被销毁。
				ASSERT(pCar->m_pEndPoint);
				if (pCar->m_nLoaded && pCar->m_pEndPoint->isStore())
					pCar->m_bChangeColor = ((StorePoint*)(pCar->m_pEndPoint))->changeColor( pCar );

				pCar->lockNode(prn, pCar);	// 20170422 唯美打包机出货点，锁定终点
			}
		}


		// 导航状态
		//PathNode & pn = path.GetAt( len - 1 );

		// 20160731 两段导航时，type == 0，所以注销下面一段
		//int type = getType( &pn );
		//if( type == 0 )
		////EndPoint * pep = findEndPoint( m_arrRoadNodes[index-1] );
		////if( pep < 0 )	// 异常
		//{
		//	*pResult = -22;	//L"找不到AGV所在节点"
		//	return;
		//}

		// 20160731 两段导航时，type == 0，不是端点，加上条件
		// 20170412 不分两段，20170508 改回两段导航
		// 20170721 取消返程避让
		/*if( pn.m_nCard < 0 )	// 是端点
		{
			RoadNode * prn = &m_arrRoadNodes[pn.m_nID-1];
			pCar->setEndNode( prn, m_arrEndPoint[pn.m_nPointID - 1] );	// 第一个参数必须从m_arrRoadNodes中取得，不能直接用pn，因为pn有可能被销毁。
			pCar->lockNode( prn, pCar );	// 20170422 唯美打包机出货点，锁定终点

			// 20160914	?? 金意陶专用	// 倒数第二个点4号点，取货后原路返回，锁定返程路
			// 20170408 注释
			int nCard = path.GetAt( len - 2 ).m_nCard;
			//if( nCard == RESERVECARD )
			//{
			//	pCar->lockNode2( nCard );
			//}

			// 20160914		// 如果存货时AGV始终占用主道，锁定返程路
			// 设定m_pCar2锁定返程道路
			// 不能用pCar->m_nRunIndex判断走到什么地方，还没有初始化
			EndPoint * pep = m_arrEndPoint[pn.m_nPointID - 1];	// 如果是端点，返回端点类型
			int nType = pep->getType();
			int dx = pep->getLength( pCar->m_pEndNode->m_nDirect );
			int ll = pCar->m_pEndNode ->m_nLength - dx;
			if( (nType & 0x80		// 存货区
				&& ll < theApp.m_nOccufied )	// 最后几垛锁定返程道路
				)
			{
				int nc1 = m_arrRoadNodes[RESERVENODE].m_nCard;	// 关键点卡号
				int nc2 = pCar->m_pReadData->nStartCard;		// AGV当前定位点
				for( int i = 0; i < len; i++ )	// 遍历以走过的路径
				{
					int nc3 = path[i].m_nCard;
					if( nc3 == nc1 )		// 如果已过关键点
						pCar->lockNode2( nCard );

					if( nc3 == nc2 )
						break;
				}
			}
		
			// 20170412		// 如果取货时AGV始终占用主道，锁定返程路
			// 设定m_pCar2锁定返程道路
			// 不能用pCar->m_nRunIndex判断走到什么地方，还没有初始化
			if( nType & 0x40 )		// 取货区
			{
				int nc1 = m_arrRoadNodes[RESERVENODE2].m_nCard;	// 关键点卡号
				int nc2 = pCar->m_pReadData->nStartCard;		// AGV当前定位点
				for( int i = 0; i < len; i++ )	// 遍历以走过的路径
				{
					int nc3 = path[i].m_nCard;
					if( nc3 == nc1 )		// 如果已过关键点
						pCar->lockNode2( nCard );

					if( nc3 == nc2 )
						break;
				}
			}
		}* /

		//if( pCar->getDisableChange() != -1 )	// 如是不是从停用转到正常，加到运行队例中，否则已经加到队列中，不再加，// ?? 逻辑不很清晰
		//{
		//	ASSERT( m_arrCars.GetCount() > m_arrRunCars.GetCount() );	// ?? 目前这两个数相等
		//	m_arrRunCars.Add( pCar );
		//}
	}
	CATCH_ALL(e)
	{
		WCHAR msg[1000];
		e->GetErrorMessage( msg, 1000 );

		// 保存日志
		GException ge;
		ge.m_nCode		= 8005;
		ge.m_strObject	= L"Ground::onCoveryCar";
		ge.m_strSummary	= msg;
		//ge.m_strDetails.Format( L"获取事件列表中鼠标行（%i）的事件对象和摘要时出错", m_nMouse );

		theApp.m_pLogList->addLog( &ge );
	}
	END_CATCH_ALL

}
*/


/// <类型>Fanction</类型>
/// <作者>Mr. Hu</作者>
/// <日期>20200507</日期>
/// <功能>开始取货</功能>
/// <参数 name = "pCar">指定AGV</参数>
/// <参数 name = "nGet">取货点，道路表ID-1</参数>
/// <返回>TRUE成功，FALSE没找到取货点</返回>
/// <说明>指定AGV开始工作，AGV从初始位置开始，找取货点，然后开始导航。
///			20181019 nGet = -1时，转到相邻位置</说明>
BOOL Ground::carStartGet(Car* pCar, int nGet)
{
	ASSERT(pCar);

	int nStart = -1;	// 起点序号号，ID-1

	{
		HLock hl(&pCar->m_mutexPath, 1, (DWORD)-27);	// 等待读，退出时自动解锁

		//ASSERT(pCar->m_arrPath.GetCount() == 2);	// 在端点等待
		size_t len = pCar->m_arrPath.GetCount();
		ASSERT(len >= 2);	// 20201102 改在>=2，因为在线充电完成后路径多
		nStart = pCar->m_arrPath[len - 1].m_nID - 1;
		//}

		// ?? 异常处理

		// 计算路径
		ASSERT(nGet >= 0);
		//if (nGet >= 0)
		//{
			// 终点和起点相同，发车时取发车第二点，或存空托完成后再取这一点
		if (m_arrRoadNodes[nGet].m_nCard == m_arrRoadNodes[nStart].m_nCard)
		{
			ASSERT(nStart == pCar->m_arrPath[len - 1].m_nID - 1);
			// 把nStart移到前一个点
			nStart = pCar->m_arrPath[len - 2].m_nID - 1;

			//pCar->pickupCurrent(nStart);	// 取当前位置的货
		}
	}
	{
		// ?? 不需要锁定路径，下级有锁定
		//HLock hl(&m_mutexPath, -1);	// 等待写，退出时自动解锁
		TRY
		{
			//m_pAStar->getPath(nStart, nGet);
			//pCar->setPath(m_pAStar->getPath());		// 可能改变pCar->m_arrPath.m_nSize;
			AStar aStar(m_arrRoadNodes);
			aStar.getPath(nStart, nGet);

			// 20210323 如果是继续向前行驶，把原路径的到数第二点加到新路径的第一点
			// 20220219 以下适用于从道路中间改道，只有发车用
			INT_PTR len1 = pCar->m_arrPath.GetCount();
			if (len1 > 1)
			{
				ASSERT(aStar.m_arrPath.GetCount() > 1);
				// 如果不是回头路，把上一次路径的倒数第二个点加新路径的第一点。
				if (
					//(pCar->m_pReadData->nState == 0x1025)								// 20220219 只有发车用
					(len1 == 2)								// 20220219 只有发车用
					&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[1].m_nCard)
					&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[0].m_nCard)		// 排除发车时反向行行驶
					)
				{
					// ?? 没有计算m_G和m_F
					aStar.m_arrPath.InsertAt(0, pCar->m_arrPath[0]);
				}
			}

			pCar->setPath(aStar.getPath(), nGet);		// 可能改变pCar->m_arrPath.m_nSize;
		}
		CATCH(GException, pge)
		{
			// 保存日志
			pge->m_strObject = pCar->getIPPort();
			theApp.m_pLogList->addLog(pge);

			return FALSE;	// 路径断开，不能到达，跳过。
		}
		CATCH_ALL(e)
		{
			HLock hl(&pCar->m_mutexPath, -1, (DWORD)-2);		// 等待写
			pCar->removePath(0, pCar->m_arrPath.GetCount());	// 删除路径，否则后面到运行

			WCHAR msg[1000];
			e->GetErrorMessage(msg, 1000);

			// 保存日志
			GException ge;
			ge.m_nCode = 2024;
			ge.m_strObject = pCar->getIPPort();
			ge.m_strSummary = msg;
			//ge.m_strDetails = getPlcStr();

			theApp.m_pLogList->addLog(&ge);

			return FALSE;

			//e->ReportError();	// 改为状态口显示，不影响正常运行
		}
		END_CATCH_ALL

		return TRUE;
	}

	/*
	// 开始行走
	// 潜伏式从中间存取货，开始运行再清初始化标志
	if (pCar->m_pReadData->sd.sd9.nCmd & 0x1000	// AGV初始化
		)
	{
		// 复位初始化信号
		pCar->m_pReadData->sd.sd9.nCmd &= ~0x1000;
		//pCar->m_pSocket->sendData1(pCar->m_pReadData->sd.sd9.nCmd);
	}

	// 20220225 把这一段移到setPath中，避免路径设置完毕，还没有设置m_pEndPoint时向PLC传数据
	//RoadNode & rn = m_arrRoadNodes[nGet];
	//if( rn.m_nPointID > 0 /*&& nGet2 < 0* /)		// 导航到端点时设置端点，到中间点时不设置，到隔壁避让不设置
	//{
	//	pCar->setEndNode( &rn, m_arrEndPoint[(INT_PTR)rn.m_nPointID - 1] );
	//}

	return TRUE;
	*/
}


/// <作者>Mr. Hu</作者>
/// <日期>20220411</日期>
/// <功能>充电转取货</功能>
/// <参数 name = "pCar">指定AGV</参数>
/// <参数 name = "nGet">取货点，道路表ID-1</参数>
/// <返回>TRUE成功，FALSE没找到取货点</返回>
/// <说明>AGV正在去充电点途中，有新的呼叫时，重新规划路径，到呼叫点</说明>
BOOL Ground::carStartGet2(Car* pCar, int nGet)
{
	ASSERT(pCar);
	ASSERT(pCar->m_pEndPoint);
	ASSERT(pCar->m_pEndPoint->isRecharge());

	int nStart = -1;	// 起点序号，ID-1
	int nSend = pCar->m_nSendIndex;						// 已经发送的数据
	{
		HLock hl(&pCar->m_mutexPath, -1, (DWORD)-27);	// 等待读，退出时自动解锁

		//ASSERT(pCar->m_arrPath.GetCount() == 2);		// 在端点等待
		size_t len = pCar->m_arrPath.GetCount();
		ASSERT(len >= 2);								// 20201102 改在>=2，因为在线充电完成后路径多
		ASSERT(nSend < len);
		if (len - nSend < 4)
			return FALSE;								// 如果后面的路径少于3段，不改路线

		nStart = pCar->m_arrPath[(INT_PTR)nSend + 1].m_nID - 1;	// 从当前发送点的下一点开始，因为发送点包含转弯，不能改

		//// 计算路径
		//ASSERT(nGet >= 0);
		//if (m_arrRoadNodes[nGet].m_nCard == m_arrRoadNodes[nStart].m_nCard)
		//{
		//	ASSERT(nStart == pCar->m_arrPath[len - 1].m_nID - 1);
		//	// 把nStart移到前一个点
		//	nStart = pCar->m_arrPath[len - 2].m_nID - 1;

		//	//pCar->pickupCurrent(nStart);	// 取当前位置的货
		//}
	}
	{
		// ?? 不需要锁定路径，下级有锁定
		//HLock hl(&m_mutexPath, -1);	// 等待写，退出时自动解锁

		//m_pAStar->getPath(nStart, nGet);
		//pCar->setPath(m_pAStar->getPath());		// 可能改变pCar->m_arrPath.m_nSize;
		AStar aStar(m_arrRoadNodes);
		aStar.getPath(nStart, nGet);

		// 删除新路径第一项，因为第一项的角度和长度是0
		CArray<PathNode, PathNode&>& starPath = aStar.m_arrPath;
		ASSERT(starPath.GetCount() > 1);
		starPath.RemoveAt(0, 1);

		// 如果不是回头路，把上一次路径的倒数第二个点加新路径的第一点。
		int nRun = pCar->m_nRunIndex - 1;			// 运行的数据
		//aStar.m_arrPath[0].m_nAngle = pCar->m_arrPath[nSend].m_nAngle;
		for(int i = nSend + 1; i >= nRun && i >= 0; i--)	// 补1-2段，已经发送和紧跟的后一段
		{
			// ?? 没有计算m_G和m_F
			starPath.InsertAt(0, pCar->m_arrPath[i]);
		}

		pCar->setPath(starPath, nGet);		// 可能改变pCar->m_arrPath.m_nSize;

		//pCar->m_nRunIndex = pCar->getRunIndex();
		//pCar->m_nSendIndex = pCar->m_nRunIndex + 1;
	}

	// 20220225 把这一段移到setPath中，避免路径设置完毕，还没有设置m_pEndPoint时向PLC传数据
	//RoadNode & rn = m_arrRoadNodes[nGet];
	//if( rn.m_nPointID > 0 /*&& nGet2 < 0*/)		// 导航到端点时设置端点，到中间点时不设置，到隔壁避让不设置
	//{
	//	pCar->setEndNode( &rn, m_arrEndPoint[(INT_PTR)rn.m_nPointID - 1] );
	//}

	return TRUE;
}

// 匹配取货点对应的存货点，用PLC控制活动行
// int nNode 起始点，道路节点序号, BOOL bChangeColor 换色号
// 返回找到的点在道路表中0开始的序号，-1表示没找到。
// ?? 没有判断库区是否被占用
int Ground::matchStore(Car* pCar)
{
	// ?? 这段程序在本线程中执行，不用锁
	// HLock hl(&m_mutexPath, 1);	// 等待读，退出时自动解锁

	ASSERT(pCar->m_arrPath.GetCount() == 2);	// 只能是端点
	RoadNode * prn = &pCar->m_arrPath[1];
	//int nStart = prn->m_nID - 1;	// 变成0开始的序号。
	EndPoint * pep0 = m_arrEndPoint[(INT_PTR)prn->m_nPointID - 1];
	EndPoint * pep1 = pep0->getNextPointAll();
	if (!pep1)
		return -1;	// 刚启动时，m_pNextPoint还没有赋值

	// 换色号，检查剩余垛数，小于3垛时换行
	ASSERT(pep1->isStore());
	BOOL bChangeLine = pep1->isFull();
	if (!bChangeLine)
	{
		BOOL bChColor = ((StorePoint*)pep1)->changeColor(pCar);	// 换色号
		pCar->m_bChangeColor = bChColor;
		if (bChColor)		// 换色号，检查剩余垛数，小于3垛时换行
		{
			// 检查两个工位中垛数
			//LPCTSTR s3 = pep1->getProductNum();
			INT_PTR n = m_arrEndPoint.GetCount();
			for (INT_PTR i = 0; i < n; i++)
			{
				EndPoint* pep3 = m_arrEndPoint[i];
				ASSERT(pep1->isStore());
				if (!pep3->isNeighbor(pep1))	// 不是同类，跳过，同类包括自己
					continue;

				// 修改相邻端点信息，包括自己
				pep3->m_strProductModel = pCar->m_strProductModel;
				pep3->m_strProductNumber = pCar->m_strProductNumber;
				pep3->m_strProductGrade = pCar->m_strProductGrade;

				if (pep3->getXCurrent() > pep3->getXNumber() - 2)	// 还剩不到3垛	// 20181107 改为2垛，省地方
				{
					bChangeLine = TRUE;
					break;
				}
			}
		}
	}

	if (!bChangeLine)	// 不换行，返回匹配的节点，否则到下一段找空行
		return pep1->getRoadNode()->m_nID - 1;	// 可用的活动行

// 没有找到可用活动的行，移到新空行
// ?? 找不到报警
	if (pep0->m_bufCode[0] == L'2')		// 清远家美专用，二次取货不自动换行
		return -1;
	else
		return changeLine(pep0);		// 一次取货自动换行
}

// 匹配取货点对应的存货点，先最少的行
// 返回找到的点在道路表中0开始的序号，-1表示没找到。
// int nNode 起始点，道路节点序号, BOOL bChangeColor 换色号
// ?? 没有判断库区是否被占用
int Ground::matchStore3(int nNode, BOOL /*bChangeColor*/)
{
	RoadNode * pNode = &m_arrRoadNodes[nNode];
	EndPoint * pep0 = m_arrEndPoint[(INT_PTR)pNode->m_nPointID - 1];

	ASSERT(!pep0->m_strNextCode.IsEmpty());	// 有子端点

	size_t lenNextCode = pep0->m_strNextCode.GetLength();
	ASSERT(!(lenNextCode & 3));	// 子端点长度是四的倍数
	PCTSTR s = pep0->m_strNextCode;

	INT_PTR n = m_arrEndPoint.GetCount();
	EndPoint * pep1 = 0;
	for (size_t i = 0; i < lenNextCode; i += 4)
	{
		for (INT_PTR j = n - 1; j >= 0; j--)
		{
			EndPoint * pep2 = m_arrEndPoint[j];
			if (*(PINT64)pep2->getCode() != *(PINT64)(s + i))		// 4个字符变成64位整数，每4个字符是一个类型
				continue;

			if (pep2->m_bDisabled || pep2->isFull())		// 信用或已满
				continue;

			if (!pep1)
			{
				pep1 = pep2;
				continue;
			}

			if (pep1->m_nXCurrent < pep2->m_nXCurrent)	// 垛数多
				continue;

			if (pep1->m_nXCurrent > pep2->m_nXCurrent)
			{
				pep1 = pep2;
				continue;
			}

			if (pep1->m_nZCurrent > pep2->m_nZCurrent)
			{
				pep1 = pep2;
				continue;
			}
		}
	}

	if (pep1)
		return pep1->getRoadNode()->m_nID - 1;	// 可用的活动行
	else
		return -1;

/*
	ASSERT(!pep0->m_strNextCode.IsEmpty());	// 有子端点
	ASSERT(!(pep0->m_strNextCode.GetLength() & 3));	// 子端点长度是四的倍数
	INT_PTR n = m_arrEndPoint.GetCount();
	//EndPoint * pep1 = 0;
	for (int j = 0; j < n; j++)
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		if (pep2->isParent(pep0)	// pep0是pep2的父端点
			&& ((!pep2->isFull()		// 未满
				&& !pep2->getRoadNode()->m_pCar)		// 没有占用
				|| pep2->getCode()[0] == L'3')	// ?? 东鹏专用，抛光线有货也去
			)
		{
			return pep2->getRoadNode()->m_nID - 1;	// 可用的活动行
		}
	}

	return -1;
	*/
}

// 匹配取货点对应的存货点，智能判断活动行
// 返回找到的点在道路表中0开始的序号，-1表示没找到。
// int nNode 起始点，道路节点序号, BOOL bChangeColor 换色号
// ?? 没有判断库区是否被占用
// 20201118 增加参数 pCar 取AGV的层数
int Ground::matchStore2(Car* pCar, int nNode)
{
	// 求道路节点对应的端点，有可能一个端点对应多个道路节点
	/*RoadNode* prn = &m_arrRoadNodes[nNode];
	int nCard = prn->m_nCard;
	int nPointID = prn->m_nPointID;
	for (size_t i = 0, length = m_arrRoadNodes.GetCount(); !nPointID && i < length; i++)
	{
		prn = &m_arrRoadNodes[i];
		if (nCard == prn->m_nCard)
			nPointID = prn->m_nPointID;
	}*/

	// 查找起始站点，同一节点可能对应多个卡号，取卡号对应的节点集合
	int nPointID = m_colls.findEndPoint(nNode);
	//RoadNode* prn = &m_arrRoadNodes[nNode];
	//int nPointID = prn->m_nPointID;
	//if(prn->m_nPointID)
	//	nPointID = prn->m_nPointID;							// 如果节点是站点，直接用
	//else
	//{														// 否则查找卡号对应的站点
	//	int nCard = prn->m_nCard;
	//	NodeColl* pnc1 = m_colls.m_nodesColl.m_pArrCards[nCard];
	//	ASSERT(pnc1);
	//	if (!pnc1)
	//		return -1;										// 没有对应的集合，返回-1

	//	// 查找站点
	//	for (size_t i = 0, length = pnc1->m_pArrRoad.GetCount(); i < length; i++)
	//	{
	//		prn = pnc1->m_pArrRoad[i];
	//		if (prn->m_nPointID)
	//		{
	//			nPointID = prn->m_nPointID;
	//			nCard = prn->m_nCard;
	//			break;
	//		}
	//	}
	//}

	// 下级集合
	ASSERT(nPointID);
	EndPoint* pep0 = m_arrEndPoint[(INT_PTR)nPointID - 1];
	ASSERT(!pep0->m_strNextCode.IsEmpty());	// 有子端点
	Collection* pColl = theApp.m_pGround->m_colls.getColl(LPCTSTR(pep0->m_bufCode));
	if (!pColl)
	{
		return -1;							// 没有存货集
	}
	pColl = pColl->m_pNextColl;				// 取下一级
	if (!pColl)
	{
		return -1;							// 没有存货集
	}

	//size_t lenNextCode = pep0->m_strNextCode.GetLength();
	//ASSERT(!(lenNextCode & 3));	// 子端点长度是四的倍数
	//PCTSTR s = pep0->m_strNextCode;

	CArray<EndPoint*>& arrPoint = pColl->m_arrPoint;
	INT_PTR n = arrPoint.GetCount();
	int nCanUse = -1;
	int nActived = -1;
	EndPoint* pUsePoint = 0;
	for (int j = 0; j < n; j++)
	{
		EndPoint* pep2 = arrPoint[j];

		//if (!pep2->isStore())
		//	continue;

		if (pep2->m_bDisabled)
			continue;

		if (pep2->isProduct())
		{
			if (pep2->allowStore(pCar))
			{
				nCanUse = pep2->getRoadNode()->m_nID - 1;	// 第一全可用的行
				pUsePoint = pep2;
				break;										// 找到存货点，跳出
			}
		}
		else
		{
			int nSerial = pep2->getRoadNode()->m_nID - 1;			// 站点到道路表中的序号，从0开始
			if (nActived < 0 && pep2->m_bActived)
				nActived = nSerial;	// 可用的活动行

			if (pep2->isFull())
				continue;

			if (nCanUse < 0)
			{
				nCanUse = nSerial;	// 第一全可用的行
				pUsePoint = pep2;
			}

			if (nActived >= 0)								// 取活动行后面的可用行
			{
				nCanUse = nSerial;	// 可用的行
				pUsePoint = pep2;
				break;
			}
		}
	}

	if (nCanUse >= 0)
	{
		ASSERT(pUsePoint);
		setPointActive(pUsePoint);
	}

	return nCanUse;

	/*RoadNode* pNode = &m_arrRoadNodes[nNode];
	EndPoint* pep0 = m_arrEndPoint[(INT_PTR)pNode->m_nPointID - 1];

	ASSERT(!pep0->m_strNextCode.IsEmpty());	// 有子端点

			// 查询只许进一个AGV的区Axxx
			//if (pep2->m_bufCode[0] == L'A')
			//{
			//	if(hasAGV(pep2, pCar))
			//		continue;
			//}

			//BOOL b1 = !pep2->isFull()		// 未满
			//	|| pep2->getCode()[0] == L'3'	// ?? 东鹏专用，抛光线有货也去
			//	|| pep2->getCode()[0] == L'T'	// ?? 电梯，有货也去
			//	;
			//if (!b1)
			//	continue;

			// 层数
			bool b2 = pep2->allowStore(pCar);	// ?? 没有取上集合的层数
			if(b2 /*&& b3* /)
				return pep2->getRoadNode()->m_nID - 1;	// 可用的活动行
		}
	}

	return -1;
	*/
}

// 换到新空行，包括一些小行
// 通过m_nSerial判断，m_nSerial相同的行都是空，表示有空行
// m_nSerial必须安顺序排
// ?? 没有类型判断，找到产品点
/*int Ground::changeLine(EndPoint * /*pep* /)
{
	INT_PTR n = m_arrEndPoint.GetCount();
	EndPoint * pep1 = 0;
	int nSerial = m_arrEndPoint[0]->m_nSerial;
	for (INT_PTR j = 0; j < n; j++)
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		if (pep2->m_nSerial < nSerial)
			continue;	// 跳过有货行

		if (pep2->m_nSerial > nSerial)	// 如果是不同的序号，找到空行
		{
			ASSERT(j > 0);
			pep1 = m_arrEndPoint[j - 1];

			// 找到空行
			setStoreActived(pep1);
			return pep1->getRoadNode()->m_nID - 1;
		}

		if (!pep2->isEmpty())			// 空
		{
			nSerial++;
		}

	}
	
	return -1;
}
*/


// 换到新空行
// 通过m_nSerial和大类判断，m_nSerial相同的行都是空，表示有空行
// EndPoint * pep 取货点
// 返回路径点的序号(id-1)
int Ground::changeLine(EndPoint * pep)
{
	if (!pep->isProduct())
		return -1;	// 不是产品点，二次倒货不换行

	//ASSERT(pep->m_pSocket);

	// 找同类空行（包括同类小行）
	INT_PTR n = m_arrEndPoint.GetCount();
	EndPoint* pep1 = pep->getNextPoint();	// 只有一次存货
	ASSERT(pep1);
	if (!pep1)			// 停用时下一点是0
		return -1;		// ?? 异常

	EndPoint* pep3 = 0;
	int nCode1 = pep1->getCode1();	// 库区线号，只在相同的库区内转换，不到不同的库区，因为产品不一样
	int nCode2 = pep1->getCode2();	// 工位号
	pep1 = 0;
	int nSerial = -1;
	for (INT_PTR j = 0; j < n; j++)
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		if (pep2->getCode1() != nCode1)	// 大类相同
			continue;

		if (pep2->m_nSerial != nSerial)
		{
			if (!!pep1)		// m_nSerial 对应的道都是空，转到这一道	// 所有道相连，中间不夹别的道
				break;

			pep1 = pep2;
			nSerial = pep2->m_nSerial;
		}
		
		if (pep2->m_pPreviousPoint || pep2->m_pPreviousPoint2 || pep2->getNextPoint2() || !pep2->isEmpty()) // 没有上一点，没有下一点，不是空
			pep1 = 0;
		else if (pep1 && pep2->getCode2() == nCode2)	// 换行后工号相同
			pep3 = pep2;
	}

	if (!pep1)
		return -1;
	
	ASSERT(pep1->isStore());
	ASSERT(pep3);

	//pep->m_pSocket->setStoreActive(pep1->m_nSerial);
	//VERIFY(!setStoreActive(pep->m_pSocket));

	return pep3->getRoadNode()->m_nID - 1;	// 返回道路表顺号
}

/*
int Ground::changeLine(EndPoint * pep)
{
	// 找取货点对应的活动行
	INT_PTR n = m_arrEndPoint.GetCount();
	EndPoint* pep1 = 0;
	EndPoint* pep3 = 0;
	for (INT_PTR j = 0; j < n; j++)
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		if (pep2->isParent(pep)			// pep是父点
#pragma aaaaaa("!!!!!!")
//			&& pep2->m_bPointActived	// 活动点
			)
		{
			pep1 = pep2;
			break;
		}
	}

	if(!pep1)		// 没有活动点	// ?? 异常处理
		return -1;

	// 找同类空行（包括同类小行）
	int nCode1 = pep1->getCode1();	// 生产线号
	int nCode2 = pep1->getCode2();	// 工位号
	pep1 = 0;
	int nSerial = -1;
	for (INT_PTR j = 0; j < n; j++)
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		if (pep2->getCode1() != nCode1)	// 大类相同
			continue;

		if (pep2->m_nSerial != nSerial)
		{
			if (!!pep1)		// m_nSerial 对应的道都是空，转到这一道
				break;

			pep1 = pep2;
			nSerial = pep2->m_nSerial;
		}

		if (!pep2->isEmpty()
#pragma aaaaaa("!!!!!!")
			//			|| pep2->m_bGet2
//			|| pep2->m_bStore2
			)
		{
			pep1 = 0;
		}
		else if (pep1 && pep2->getCode2() == nCode2)	// 换行后工号相同
			pep3 = pep2;
	}

	if (pep1)
	{
		ASSERT(pep1->isStore());
		ASSERT(pep3);
#pragma aaaaaa("!!!!!!")
		//		setStoreActived(pep1);	// 设置活动行
		return pep3->getRoadNode()->m_nID - 1;	// 返回道路表顺号
	}
	else
		return -1;
}
*/


// 取指定点的类型
/*int Ground::getType( RoadNode * pNode )
{
	if( pNode->m_nPointID == 0 )	// 是不是端点
		return 0;

	ASSERT( m_arrEndPoint.GetCount() > pNode->m_nPointID - 1 );
	return m_arrEndPoint[pNode->m_nPointID - 1]->getType();	// 如果是端点，返回端点类型
}
*/

// 取指定类型的点， int nStart 查找的起始位置, int nType 类型，返回找到的节点索引，没找到返回-1
/*int Ground::getNode( int nStart, int nType )
{
	INT_PTR n = m_arrRoadNodes.GetCount();
	for( int i = nStart; i < n ; i++ )
	{
		RoadNode & rn = m_arrRoadNodes[i];
		if( getType( &rn ) & nType )		// 对应区
		{
			EndPoint * pep2 = m_arrEndPoint[rn.m_nPointID-1];
			if( !pep2->isFull() )	// 未满
				return i;
		}
	}
	return -1;
}
*/

// 连接端点PLC，要求先装入端点表
void Ground::getPointPLC()
{
	/*
	INT_PTR n = m_arrEndPoint.GetCount();
	for( int i = 0; i < n; i++ )
	{
		EndPoint * pep = m_arrEndPoint[i];
		if( pep->getPLCIP().IsEmpty() )	// 端点表没有IP，继续
			continue;

		INT_PTR m = m_arrPointSocket.GetCount();
		int j = 0;
		for (; j < m; j++)
		{
			PointSocket* pps = m_arrPointSocket[j];

			if (pps->m_strIP != pep->getPLCIP()
				|| pps->m_nPort != pep->getPLCPort()
				)
				continue;

			pep->setSocket(pps);
			//pps->m_nTray = pep->m_nTray;	// 上次的托号	// 20200327 不取上次托号，从数据据库中取

			// 按顺序插入到m_arrPoint
			INT_PTR length = pps->m_arrPoint.GetCount();
			INT_PTR ii = 0;
			for (; ii < length; ii++)
			{
				if (pep->m_nPLCAdd < pps->m_arrPoint[ii]->m_nPLCAdd)				// 如果表中的地址大于站点地址，往前找
				{
					pps->m_arrPoint.InsertAt(ii, pep);								// 20201110 端点存到Socket中
					break;
				}
			}
			if (ii >= length)
			{
				pps->m_arrPoint.Add(pep);
			}

			break;
		}

		if (j == m)
		{
			ASSERT(getWnd());	// 这时getWnd()->HWnd = 0，还没有建立窗口
			PointSocket* pps = (theApp.m_bDemo || theApp.m_bDemoPoint)
				? new PointDemo(getWnd(), pep->getPLCIP(), pep->getPLCPort())	// 测试状态，启动Demo
				: new PointSocket(pep->getPLCIP(), pep->getPLCPort());
			m_arrPointSocket.Add(pps);
			pep->setSocket(pps);
			//pps->m_nTray = pep->m_nTray;	// 上次的托号	// 20200327 不取上次托号，从数据据库中取
			pps->m_arrPoint.Add(pep);	// 20201110 端点存到Socket中
		}

		// ?? 停用时，点和plc数数据不同
		//if(pep->m_pSocket && pep->m_pSocket->m_bDisabled != pep->m_bDisabled)
		//	setProductDisabled(pep->m_pSocket, pep->m_pSocket->m_bDisabled);
		//PBYTE pb = (PBYTE)pep->m_pSocket->getPlcData();
		// 存货呼叫，什么也不画
		//pep->m_bDisabled = pb[pep->m_nPLCAdd] & 0x0008;		// H0.04 存货呼叫
	}
	*/
}

// 创建窗口后执行该事件
void Ground::initCtrl()
{
	HCtrl::initCtrl();

	//INT_PTR n = m_arrPointSocket.GetCount();
	//for( int i = 0; i < n; i++ )
	//{
	//	CWinThread * pThread = AfxBeginThread( PointSocket::threadPro, (LPVOID)m_arrPointSocket[i], THREAD_PRIORITY_TIME_CRITICAL );
	//	m_arrPointSocket[i]->m_pThread = pThread;
	//}

	// 最高优先级
	//m_pThreadWcs = AfxBeginThread(SocketWcs::threadPro, (LPVOID)&m_socketWcs, THREAD_PRIORITY_TIME_CRITICAL);
	// 普通优先级
	//m_pThreadWcs = AfxBeginThread(SocketWcs::threadPro, (LPVOID)&m_socketWcs, THREAD_PRIORITY_TIME_CRITICAL);
	m_socketWcs.beginThread();
}

/// <类型>Fanction</类型>
/// <作者>Mr. Hu</作者>
/// <日期>20200327</日期>
/// <功能>获取中间点对应的端点</功能>
/// <参数 name = "nCard">中间点对应卡号</参数>
/// <返回>查到的第一个端点指针，如果没有，返回0</返回>
/// <说明></说明>
ProductPoint* Ground::getPointFromMidle(int nCard)
{
	INT_PTR i = m_arrEndPoint.GetCount() - 1;
	for (; i >= 0; i--)
	{
		EndPoint* pep = m_arrEndPoint[i];
		if (!pep->IsKindOf(RUNTIME_CLASS(ProductPoint)))	// 不是产品类，跳过
			continue;

		int n中间点 = pep->m_nMiddleID > 0 ? pep->m_nMiddleID - 1 : -1;
		if (n中间点 < 0 || m_arrRoadNodes[n中间点].m_nCard != nCard)	// 不是中间点对应的端点，跳过
			continue;

		return (ProductPoint*)pep;
	}

	return 0;	// 没找到
}

// 查找与指定卡号中间点相同的空闲取货点，有货
int Ground::matchProduct2( int nCard )
{
	INT_PTR i = m_arrEndPoint.GetCount() - 1;
	for( ; i >= 0; i-- )
	{
		EndPoint * pep = m_arrEndPoint[i];
		if( !pep->IsKindOf( RUNTIME_CLASS(ProductPoint)) )	// 不是产品类，跳过
			continue;

		int n中间点 = pep->m_nMiddleID > 0 ? pep->m_nMiddleID - 1 : -1;
		if(n中间点 < 0 || m_arrRoadNodes[n中间点].m_nCard != nCard )	// 不是中间点对应的端点，跳过
			continue;

		RoadNode * prn = pep->getRoadNode();
		//pep->getLength(FALSE);	// 借用这个函数计算正在工作的垛，?? 重启后如果有新垛可能计算不准确，假定是取货
										// 20170409 同时计算取货点的取色号（产品编号）
		BOOL 有货 = (((ProductPoint*)pep)->m_nPile != 0);
		if( 有货 )
			return prn->m_nID - 1;	// 索引号
	}

	return -1;	// ?? 一定有
}

// 查找匹配通讯的端点 PointSocket * pSocket 取货点通讯类，返回端点指针
EndPoint * Ground::matchProduct( PointSocket * /*pSocket*/ )
{
	11111111111111111;
	/*
	EndPoint * pep11 = 0;
	INT_PTR n = m_arrEndPoint.GetCount();
	for( int ii = 0; ii < n; ii++ )
	{
		pep11 = m_arrEndPoint[ii];
		if( pep11->m_pSocket == pSocket )	// 匹配通讯口
			return pep11;
	}

	ASSERT( pep11 );	// ?? 异常处理
	*/
	return 0;
}

// 查找空闲的取货点
// 20170508 增加中间等待点功能
// 返回最远的没有被占用的有货点，都没货返回最远点。
// int nStart 有对应点优先，-1，任意
// 2017年0417注释，适合抛光下砖机取货条件，一条线上多个工位，每个工位不同色号。加了一项包装机下砖取货
// 20170424 改回原来的方法，用端点号对就不同的区
// 20170613 如果有中间点，取货过程中不再派车
// 20180104 删除，改用getProduct 
/*int Ground::matchProduct( int nStart )
{
	// 等待时刻，先设一个较大的数，越小时间越长
	COleDateTime dtWait( 9999, 1, 1, 0, 0, 0 );

	// 起点类型
	CString strProductType0;
	if( nStart >= 0 )
	{
		ASSERT( m_arrRoadNodes[nStart].m_nPointID != 0 );	//?? 异常
		EndPoint * pep0 = m_arrEndPoint[m_arrRoadNodes[nStart].m_nPointID - 1];	// pNode->m_nPointID 从1开始
		ASSERT( pep0 );
		strProductType0 = pep0->getProductType();
	}

	// 从后往前找
	int r = -1;
	int r1 = -1;
	int r2 = -1;
	int r3 = -1;
	int r4 = -1;
	int r5 = -1;

	int i = m_arrEndPoint.GetCount() - 1;
	for( ; i >= 0; i-- )
	{
		EndPoint * pep = m_arrEndPoint[i];
		if( !pep->IsKindOf( RUNTIME_CLASS(ProductPoint)) )	// 不是产品类，跳过
			continue;

		if( pep->m_bDisabled )	// 停用，跳过
			continue;

		Car * pCar2 =  pep->getRoadNode()->m_pCar;
		if( pCar2 && !pCar2->getDisable() )	// 被其它AGV占用，跳过
			continue;

		// 停用，跳过，h3X.00	// 20170221 暂时不考虑停用
		//PWORD pp = pep->m_pSocket->getPlcData();
		//if( (int(pp) > 0)	/* && !(pp[pep->getPLCAdd() + 30] & 0x0001)* )	// 未停用	// 20170221 暂时不考虑停用
		//	continue;
		RoadNode * prn = pep->getRoadNode();
		r = prn->m_nID - 1;	// 索引号

		pep->getLength(prn->m_nDirect);	// 借用这个函数计算正在工作的垛，?? 重启后如果有新垛可能计算不准确
										// 20170409 同时计算取货点的取色号（产品编号）
		BOOL 有货 = (((ProductPoint*)pep)->m_nPile != 0);
		
		// 类型空或类型相同统称类型相同
		CString strType = pep->getProductType();
		BOOL 类型相同 = strProductType0.IsEmpty() || strType.IsEmpty() || ( strProductType0 == strType );

		int n中间点 = pep->m_nMiddleID > 0 ? pep->m_nMiddleID - 1 : -1;
		BOOL 有中间点 = n中间点 >= 0;
		BOOL 中间点占用 = FALSE;
		int j = m_arrCars.GetCount() - 1;
		for( ; 有中间点 && j >= 0; j-- )
		{
			pCar2 = m_arrCars[j];
			if( pCar2->getDisable() )
				continue;

			int nn = pCar2->m_arrPath.GetCount();
			if( nn > 0 )
			{
				// 导航到中间点
				int c1 = pCar2->m_arrPath[nn-1].m_nCard;
				int c2 = m_arrRoadNodes[n中间点].m_nCard;
				if( c1 == c2 )
				{
					中间点占用 = TRUE;
					break;
				}

				// 导航到取货，过中间点
				EndPoint * pep2 = pCar2->m_pEndPoint;
				if( pep2 && pep2->m_nMiddleID - 1 == n中间点 )
				{
					中间点占用 = TRUE;
					break;
				}
			}
		}

		// 等待时间长
		BOOL 等待长 = pep->m_dtWait <= dtWait;
		if( 等待长 )
		{
			dtWait = pep->m_dtWait;
		}

		// 兼容有中间等待点和换中间点两种模式
		if( 有货 && !中间点占用 && 类型相同 )
			r1 = 有中间点 ? n中间点 : r;
		else if( 有货 && !中间点占用 )
			r2 = 有中间点 ? n中间点 : r;
		else if( 有中间点 && !中间点占用 && 等待长 )
			r3 = n中间点;
		else if( 有中间点 && !中间点占用 )
			r4 = n中间点;
		else if( !有中间点 && 等待长 )
			r5 = r;
		//else if( 有中间点 && 等待长 )
		//	r5 = n中间点;
	}

	return r1 < 0 ? r2 < 0 ? r3 < 0 ? r4 < 0 ? r5 < 0 ?
		-1 : r5 : r4 : r3 : r2 : r1;	// 不满足条件，返回-1，不取点。
}*/

// 查找空闲的取货点
// 20170508 增加中间等待点功能
// 返回最远的没有被占用的有货点，都没货返回最远点。
// int nStart 有对应点优先，-1，任意
// 2017年0417注释，适合抛光下砖机取货条件，一条线上多个工位，每个工位不同色号。加了一项包装机下砖取货
// 20170424 改回原来的方法，用端点号对就不同的区
/*int Ground::matchProduct( int nStart )
{
	// 等待时刻，先设一个较大的数，越小时间越长
	COleDateTime dtWait( 9999, 1, 1, 0, 0, 0 );

	// 起点类型
	CString strProductType0;
	if( nStart >= 0 )
	{
		ASSERT( m_arrRoadNodes[nStart].m_nPointID != 0 );	//?? 异常
		EndPoint * pep0 = m_arrEndPoint[m_arrRoadNodes[nStart].m_nPointID - 1];	// pNode->m_nPointID 从1开始
		ASSERT( pep0 );
		strProductType0 = pep0->getProductType();
	}

	// 从后往前找
	// 产品点，未被其它AGV占用
	// r， 记录端点
	// r1，有货，无中间点，或有中间点未占用，并且类型相同
	// r2，有货，无中间点，或有中间点未占用
	// r3，有中间点，并且未被占用，等待时间长
	// r4，无中间点，等待时间长
	// r5，有中间点
	// 优先级 r1 r2 r3 r4 r5 r
	int r = -1;
	int r1 = -1;
	int r2 = -1;
	int r3 = -1;
	int r4 = -1;
	int r5 = -1;

	int i = m_arrEndPoint.GetCount() - 1;
	for( ; i >= 0; i-- )
	{
		EndPoint * pep = m_arrEndPoint[i];
		if( !pep->IsKindOf( RUNTIME_CLASS(ProductPoint)) )	// 不是产品类，跳过
			continue;

		if( pep->getRoadNode()->m_pCar )	// 被其它AGV占用，跳过
			continue;

		// 停用，跳过，h3X.00	// 20170221 暂时不考虑停用
		//PWORD pp = pep->m_pSocket->getPlcData();
		//if( (int(pp) > 0)	/* && !(pp[pep->getPLCAdd() + 30] & 0x0001)* )	// 未停用	// 20170221 暂时不考虑停用
		//	continue;
		RoadNode * prn = pep->getRoadNode();
		r = prn->m_nID - 1;	// 索引号

		pep->getLength(prn->m_nDirect);	// 借用这个函数计算正在工作的垛，?? 重启后如果有新垛可能计算不准确
										// 20170409 同时计算取货点的取色号（产品编号）
		BOOL 有货 = (((ProductPoint*)pep)->m_nPile != 0);
		
		// 类型空或类型相同统称类型相同
		CString strType = pep->getProductType();
		BOOL 类型相同 = strProductType0.IsEmpty() || strType.IsEmpty() || ( strProductType0 == strType );

		int n中间点 = pep->m_nMiddleID > 0 ? pep->m_nMiddleID - 1 : -1;
		BOOL 有中间点 = n中间点 >= 0;
		BOOL 中间点占用 = FALSE;
		int j = m_arrCars.GetCount() - 1;
		for( ; 有中间点 && j >= 0; j-- )
		{
			Car * pCar2 = m_arrCars[j];
			int nn = pCar2->m_arrPath.GetCount();
			if( nn > 0 )
			{
				// 导航到中间点
				int c1 = pCar2->m_arrPath[nn-1].m_nCard;
				int c2 = m_arrRoadNodes[n中间点].m_nCard;
				if( c1 == c2 )
				{
					中间点占用 = TRUE;
					break;
				}
			}
		}

		// 等待时间长
		BOOL 等待长 = pep->m_dtWait < dtWait;
		if( 等待长 )
		{
			dtWait = pep->m_dtWait;
		}

		// 兼容有中间等待点和换中间点两种模式
		if( 有货 && !中间点占用 && 类型相同 )
			r1 = 有中间点 ? n中间点 : r;
		else if( 有货 && !中间点占用 )
			r2 = 有中间点 ? n中间点 : r;
		else if( 有中间点 && !中间点占用/* && 等待长* )
			r3 = n中间点;
		else if( !有中间点 && 等待长 )
			r4 = r;
		else if( 有中间点 && 等待长 )
			r5 = n中间点;
	}

	return r1 < 0 ? r2 < 0 ? r3 < 0 ? r4 < 0 ? r5 < 0 ?
		r : r5 : r4 : r3 : r2 : r1;
}
*/

// 查找空闲的取货点
// 返回最远的没有被占用的有货点，都没货返回最远点。
// int nStart 有对应点优先，-1，任意
// 2017年0417注释，适合抛光下砖机取货条件，一条线上多个工位，每个工位不同色号。加了一项包装机下砖取货
// 20170424 改回原来的方法，用端点号对就不同的区
/*int Ground::matchProduct( int nStart )
{
	int r1 = -1;	// 所有取货点
	int r2 = -1;	// 空取货点
	int r3 = -1;	// 对应取货点

	CString strProductType0;

	EndPoint * pep0 = 0;
	if( nStart >= 0 )
	{
		ASSERT( m_arrRoadNodes[nStart].m_nPointID != 0 );
		pep0 = m_arrEndPoint[m_arrRoadNodes[nStart].m_nPointID - 1];	// pNode->m_nPointID 从1开始
		ASSERT( pep0 );
		strProductType0 = pep0->getProductType();
	}


	// 从后往前找
	int i = m_arrEndPoint.GetCount() - 1;
	for( ; i >= 0; i-- )
	{
		EndPoint * pep = m_arrEndPoint[i];
		if( pep->IsKindOf( RUNTIME_CLASS(ProductPoint))	// 产品类
			//&& pep->getType() & 0x40				// 取货点
			&& !(pep->getRoadNode()->m_pCar)	// 没有被占用
			//&& ((ProductPoint*)pep)->getPile()	// 有货
			)
		{
			// 停用，h3X.00	// 20170221 暂时不考虑停用
			//PWORD pp = pep->m_pSocket->getPlcData();
			//if( (int(pp) > 0)	/* && !(pp[pep->getPLCAdd() + 30] & 0x0001)* )	// 未停用	// 20170221 暂时不考虑停用
			{
				r1 = pep->getRoadNode()->m_nID - 1;	// 索引号
				BOOL 有货 = pep->getXCurrent();
				BOOL 类型空 = strProductType0.IsEmpty();	// 类型空，匹配任意
				BOOL 类型相同 = strProductType0 == pep->getProductType();
				if( 有货 && (类型空 || 类型相同) )
					return r1;	// 有货并且类型相同，直接返回
				else if(类型空 || 类型相同)
					r2 = r1;
				else if( 有货 )
					r3 = r1;
			}
		}
	}

	return r3 < 0 ? r2 < 0 ? r1 : r2 : r3;
}
*/

// 20170417 按唯美定制 包装机下砖取货，每个包装机两个工位，每次取一边
// 20170424 改回原来的方法，用端点号对就不同的区
/*int Ground::matchProduct( int nStart )
{
	int r1 = -1;	// 所有取货点
	int r2 = -1;	// 空取货点
	int r3 = -1;	// 对应取货点

	CString strProductType0;

	EndPoint * pep0 = 0;
	if( nStart >= 0 )
	{
		ASSERT( m_arrRoadNodes[nStart].m_nPointID != 0 );
		pep0 = m_arrEndPoint[m_arrRoadNodes[nStart].m_nPointID - 1];	// pNode->m_nPointID 从1开始
		ASSERT( pep0 );
		strProductType0 = pep0->getProductType();
	}

	// 所有取货点，从后往前找
	int i = m_arrEndPoint.GetCount() - 1;
	int nCount = MAXINT;	// 取货数
	for( ; i >= 0; i-- )
	{
		// ?? Class判断
		EndPoint * pep = m_arrEndPoint[i];
		BOOL b = pep->IsKindOf( RUNTIME_CLASS(ProductPoint))	// 产品类
			&& pep->getType() & 0x40				// 取货点
			&& !(pep->getRoadNode()->m_pCar)	// 没有被占用
			//&& ((ProductPoint*)pep)->getPile()	// 有货
			;
		if( !b )
			continue;	// 不合条件，路过

		ProductPoint * ppp = (ProductPoint *)pep;
		if( ppp->m_nCount <= nCount )	// 取货少的优先，两个工位轮流取。
		{
			r1 = pep->getRoadNode()->m_nID - 1;
			nCount = ppp->m_nCount;
		}
		if( pep->getXCurrent() )	// 有货
			r2 = r1;

		if( strProductType0 == pep->getProductType() )
		{
			if( r2 == r1 )	// 同类并且有货最优先
				return r2;
			else
				r3 = r1;
		}
	}

	//return r2 >= 0 ? r2 : r1;	// 有货的优先
	//return r2;		// 只返回有货的
	//return r3 >= 0 ? r3 : r2;	// 同类优先，没货也取

	return r3 < 0 ? r2 < 0 ? r1 : r2 : r3;

	/*
	int i = -1;
	while( (i = getNode( i+1, 0x40 )) >= 0 )	// 找对应终点
	{
		RoadNode & rn = m_arrRoadNodes[i];
		if( !rn.m_pCar )
			return i;
	}
	return -1;
	*
}
*/

// 车上有货时返回对应终点，否则返回0，Car * pCar 指定的车 
BOOL Ground::hasProduct( Car * pCar )
{
	return pCar->m_bLoaded != 0;

	//if (pCar->m_nLoaded)		// 有货标志
	//	return pCar->getEndNode();		// 返回导航终点
	//else
	//	return 0;

	/*
	if( pCar->getEndPoint() && (pCar->getEndPoint()->getType() & 0x00000080) )	// 有终点，并且终点类型是存货
		return pCar->getEndNode();		// 返回导航终点

	RoadNode * prn = &pCar->m_roadNode;
	if( !prn->m_nID )		// 没有道路
		return 0;

	prn = &m_arrRoadNodes[prn->m_nID-1+1];	// -1是当前点，+1取下一个点，应该是端点，这么写便于阅读，编译时会优化掉
	if( !prn->m_nPointID )	// 不是端点
		return 0;

	ASSERT( m_arrEndPoint.GetCount() > prn->m_nPointID - 1 );
	if( getType( prn ) & 0x00000040 && (pCar->m_pReadData->nState & 0x1000) )	// 停在取货处
		return prn;

	return 0;
	*/
}

// 重新设置叉车状态
afx_msg void Ground::onDisableCar(UINT nID)
{
	int n = nID - TID_DISABLECAR;
	ASSERT( n < m_arrCars.GetCount() );

	m_arrCars[n]->disableCar(TRUE);
}

// 暂停
afx_msg void Ground::onPauseCar(UINT nID)
{
	int n = nID - TID_PAUSECAR;
	ASSERT( n < m_arrCars.GetCount() );

	m_arrCars[n]->pauseSwitch();
}

// 填充全部库区
void Ground::onFillAll()
{
	TRY
	{
		if( AfxMessageBox( L"确定要填充全部库区吗？", MB_OKCANCEL ) != IDOK )
			return;

		//CString strsql = L"UPDATE `point` SET `xCurrent` = `xNumber`, `zCurrent` = `zNumber` WHERE `type` = 128";
		//theApp.getDatabase()->Execute( strsql );

		INT_PTR len = m_arrEndPoint.GetCount();
		for (int i = 0; i < len; i++)
		{
			EndPoint* pep = m_arrEndPoint[i];
			if (!pep->isStore())
				continue;	// 不是库区，不填

			((StorePoint *)pep)->fillPoint();
		}
	}
	CATCH_ALL( e )
	{
		WCHAR msg[1000];
		e->GetErrorMessage( msg, 1000 );

		// 保存日志
		GException ge;
		ge.m_nCode		= 8003;
		ge.m_strObject	= L"Ground::onFillAll";
		ge.m_strSummary	= msg;
		ge.m_strDetails = L"填充全部存储区异常";

		theApp.m_pLogList->addLog( &ge );

		e->ReportError();
	}
	END_CATCH_ALL
}

// 清空全部库区
void Ground::onClearAll()
{
	TRY
	{
		if (AfxMessageBox(L"确定要清除全部库区吗？", MB_OKCANCEL) != IDOK)
			return;

	//TimerCounter tc;
	// 运行时产生警告 dbcore.cpp(2843) : AppMsg - Warning: Missing ' FROM ', recordset not updatable. 不影响结果
	theApp.getDatabase()->Execute(L"select funClearAll()");
	//tc.getCounter();	// 1ms funClearAll 是空，6ms 正常执行，12ms 满垛

		INT_PTR len = m_arrEndPoint.GetCount();
		for( int i = 0; i < len; i++ )
		{
			EndPoint & ep = *m_arrEndPoint[i];
			ep.setXCurrent( 0 );
			ep.setZCurrent( 0 );
		}

		// 传到下位机
		//PointSocket * pSocket = 0;
		//for( int i = 0; i < len; i++ )
		//{
		//	EndPoint * pep = m_arrEndPoint[i];
		//	if( pep->m_pSocket == pSocket )
		//		continue;
		//	if (theApp.m_bPlcCtrl)		// PLC控制垛数，库区行数小于10时适用
		//	{
		//		pSocket = pep->m_pSocket;
		//		downPile(pep);
		//	}
		//}
	}
	CATCH_ALL( e )
	{
		WCHAR msg[1000];
		e->GetErrorMessage( msg, 1000 );

		// 保存日志
		GException ge;
		ge.m_nCode		= 8004;
		ge.m_strObject	= L"Ground::onClearAll";
		ge.m_strSummary	= msg;
		ge.m_strDetails = L"清空全部存储区异常";

		theApp.m_pLogList->addLog( &ge );

		e->ReportError();
	}
	END_CATCH_ALL
}

void Ground::lbuttonDblClk( UINT nFlags, const CPoint &point )
{
	HCtrl::lbuttonDblClk( nFlags, point);	// 一定要加这一句，否窗口不响应鼠标事件

	onPauseAll();
}

// 请求运行，LPARAM Car * this
/*void Ground::onQuestRun(LPNMHDR pNMHDR, LRESULT* pResult)
{
	*pResult = 0;

	// 取AGV信息
	ASSERT(((HCtrl*)(PHNM(pNMHDR)->lParam))->IsKindOf(RUNTIME_CLASS(Car)));
	Car* pCar = (Car*)(PHNM(pNMHDR)->lParam);

	questRun(pCar);
}*/

/// <作者>Mr. Hu</作者>
/// <日期>20210204</日期>
/// <功能>请求运行</功能>
/// <参数 name = "pCar">指定的AGV</参数>
/// <返回>1 成功，0 没有任务</返回>
/// <说明></说明>
bool Ground::questRun(Car * pCar)
{
	// 不向下一级传送消息

	// ?? 这个地方写锁定范围太大，中间还有一些地方读写，会互锁卡死
	//HLock hl(&pCar->m_mutexPath, 1, -28);	// 等待写，退出时自动解锁

	//if (pCar->m_arrPath.GetCount() != 2)	// 20201102 注释，因为在线充电时，路径长度多
	//	return;		// 有可能多个AGV同时发消息，开始运行后仍可能收到消息，跳过。

	TRY
	{
		// ?? 这个地方写锁定范围太大，中间还有一些地方读写，会互锁卡死
		//HLock hl(&pCar->m_mutexPath, -1, -28);	// 等待写，退出时自动解锁

		// 存货
		if (hasProduct(pCar))
		{
			return !carStartPut(pCar);	// 用PLC控制活动条
		}

		// 充电	// 20220307 暂时取消
		//if (pCar->isLowBattery())
		//{
		//	carRecharge(pCar);
		//	return;
		//}

		// 取货
		// ?? 东鹏定制，先到相邻工位取货
		//if (pickNeighbor(pCar))
		//	return;

		// 智能取货
		// 20171228 改为从取货点选择合理的AGV
		// 20171229 从carStartGet改为pickup
		// 20190129 参数改为0，取最近的AGV
		if( pickup(pCar) )
			return true;

		return carRecharge(pCar);

		//pickup(0);		// 清远家美专用，参数为0时找最近的AGV，不会出现取自己所在的行
	}
	CATCH_ALL( e )
	{
		WCHAR msg[1000];
		e->GetErrorMessage( msg, 1000 );

		// 保存日志
		GException ge;
		ge.m_nCode		= 2025;
		ge.m_strObject	= pCar->getIPPort();
		ge.m_strSummary	= msg;
		ge.m_strDetails = L"请求运行异常，Ground::onQuestRun";

		theApp.m_pLogList->addLog( &ge );

		//pCar->m_bCanRecharge = FALSE;

		//e->ReportError();	// 会不停弹出对话框

		return false;
	}
	END_CATCH_ALL
}

/// <作者>Mr. Hu</作者>
/// <日期>20220109</日期>
/// <功能>取货完成后存到指定点</功能>
/// <参数 name = "pCar">指定AGV</参数>
/// <参数 name = "nSerial">指定点的序号</参数>
/// <返回>返回0，正常，否则不正常</返回>
/// <说明>只到指定存货端点有效，端点类型7</说明>
int Ground::carStartPut(Car* pCar, int nSerial)
{
	// 在本线程中执行，没有并发问题
	//HLock hl(&m_mutexStore, -1, 0);
	//if (!hl.allowed())
	//	return 0;	// 锁不住跳过

	// 查找对应的端点
	EndPoint* pep = 0;
	ASSERT(pCar->m_pEndPoint);				// 20220906 注释
	for (size_t i = 0, length = m_arrEndPoint.GetCount(); i < length; i++)
	{
		EndPoint* pep2 = m_arrEndPoint[i];
		if (pep2->isSpecify()
			&& pep2->m_nSerial == nSerial
			&& (!pCar->m_pEndPoint								// 重新规划路径时m_pEndPoint = 0
				|| (pCar->m_pEndPoint->m_nSerial != nSerial)	// 排除当前终点
				)
			)
		{
			pep = pep2;
			break;
		}
	}
	
	if (!pep)
	{
		return 1;					// 返回1 无存货点
	}
	//else if (!pep->allowStore(pCar)	// 指定位置允许存货
	//{
	//	return 2;					// 返回2，不允许存货
	//}

	// 取终点序号 id-1
	int nPut = (int)pep->m_pRoadNode->m_nID - 1;

	// 取起点序号
	// 20220906 发车改点
	//if (pCar->m_pEndPoint)
	ASSERT(pCar->m_pEndPoint);
	int nStart = pCar->m_pEndPoint->m_pRoadNode->m_nID - 1;

	// 计算路径
	AStar aStar(m_arrRoadNodes);
	aStar.getPath(nStart, nPut);

	// 20210323 如果是继续向前行驶，把原路径的到数第二点加到新路径的第一点
	// 20220219 以下适用于从道路中间改道，只有发车用
	INT_PTR len1 = pCar->m_arrPath.GetCount();
	if (len1 > 1)
	{
		ASSERT(aStar.m_arrPath.GetCount() > 1);
		// 如果不是回头路，把上一次路径的倒数第二个点加新路径的第一点。
		if (
			//(pCar->m_pReadData->nState == 0x1025)								// 20220219 只有发车用
			(len1 == 2)								// 20220219 只有发车用
			&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[1].m_nCard)
			&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[0].m_nCard)		// 排除发车时反向行行驶
			)
		{
			// ?? 没有计算m_G和m_F
			aStar.m_arrPath.InsertAt(0, pCar->m_arrPath[0]);
		}
	}

	pCar->setPath(aStar.getPath(), nPut);		// 可能改变pCar->m_arrPath.m_nSize;

	// 开始行走
	RoadNode* prn = &m_arrRoadNodes[nPut];
	pCar->setEndNode(prn, m_arrEndPoint[(INT_PTR)prn->m_nPointID - 1]);

	return 0;
}

/// <作者>Mr. Hu</作者>
/// <日期>20220426</日期>
/// <功能>运行过程中重新规划路线</功能>
/// <参数 name = "pCar">指定AGV</参数>
/// <参数 name = "nSerial">指定点的序号</参数>
/// <返回>返回0，正常，否则不正常</返回>
/// <说明>只到指定存货端点有效，端点类型7</说明>
/*int Ground::carRestart(Car* pCar, int nSerial)
{
	// 在本线程中执行，没有并发问题
	//HLock hl(&m_mutexStore, -1, 0);
	//if (!hl.allowed())
	//	return 0;	// 锁不住跳过

	// 查找对应的端点
	EndPoint* pep = 0;
	for (size_t i = 0, length = m_arrEndPoint.GetCount(); i < length; i++)
	{
		EndPoint* pep2 = m_arrEndPoint[i];
		if (pep2->m_nSerial == nSerial
			&& pep2->isSpecify()
			)
		{
			pep = pep2;
			break;
		}
	}

	if (!pep)
	{
		return 1;					// 返回1 无存货点
	}

	// 取终点序号 id-1
	int nEnd = (int)pep->m_pRoadNode->m_nID - 1;

	// 取起点序号
	int nStart = pCar->m_arrPath[0].m_nID - 1;

	// 终点和起点相同，发车时取发车第二点，或存空托完成后再取这一点
	if (m_arrRoadNodes[nEnd].m_nCard == m_arrRoadNodes[nStart].m_nCard)
	{
		// 把nStart移到后一个点
		nStart = pCar->m_arrPath[1].m_nID - 1;
	}

	// 计算路径
	AStar aStar(m_arrRoadNodes);
	aStar.getPath(nStart, nEnd);

	// 从1点规划路线，第2点与原路线不同，把第2点加到新路径中，作为起点
	bool b0 = (nStart == (pCar->m_arrPath[0].m_nID - 1))
		&& pCar->m_arrPath[1].m_nCard != aStar.m_arrPath[1].m_nCard
		;
	// 从2点规划路线，第1点与原路线不同，把第1点加到新路径中，作为起点
	bool b1 = (nStart == (pCar->m_arrPath[1].m_nID - 1))
		&& pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[1].m_nCard
		;

	ASSERT(!b0 || !b1);
	if (
		b0 || b1
		)
	{
		// 把原路径的第二点插入到新路径的第0点
		// ?? 没有计算m_G和m_F
		aStar.m_arrPath.InsertAt(0, pCar->m_arrPath[b0]);		// b0是0或1
		// 第二点的角度与第一点相同，反向时角度取反
		//if(aStar.m_arrPath[1].m_nSerial > aStar.m_arrPath[0].m_nSerial)
		//	aStar.m_arrPath[1].m_nAngle = REVERSE_DEG(aStar.m_arrPath[0].m_nAngle);

		PathNode& pn0 = aStar.m_arrPath[0];
		PathNode& pn1 = aStar.m_arrPath[1];

		// 反向行驶
		pn0.m_nAgvAngle = REVERSE_DEG(pn0.m_nAgvAngle);
		// 第二点与第一点角度一致
		pn1.m_nAgvAngle = pn0.m_nAgvAngle;
		// 第二点与第一点方向一致，推拉和左右横移，这里是画图的方向，在calPath中转换成运行方向
		//aStar.m_arrPath[1].m_nDirect = aStar.m_arrPath[0].m_nDirect;
		// 设置两点的距离
		pn1.m_nLength = pn0.m_nLength;		// 因为是反向，原第二点的距离是0，第一点是正常距离
		pn0.m_nLength = 0;					// 把第一点设成0


		// 第二点的道路索引和路号与第一点一致
		int len = (int)m_arrRoadNodes.GetCount();
		int np = pn0.m_nID - 1 - 1;	// 多减1把ID号变成序号
		int nn = pn0.m_nID + 1 - 1;
		if (pn0.m_nID > 1
			&& m_arrRoadNodes[np].m_nCard == pn1.m_nCard
			)
		{
			// 与上一点卡号相同，则用上一点的ID号和路号
			pn1.m_nID = pn0.m_nID - 1;
			pn1.m_nRoad = m_arrRoadNodes[np].m_nRoad;
			pn1.m_fRoadRad = m_arrRoadNodes[np].m_fRoadRad;
		}
		else if (pn0.m_nID < len - 1
			&& m_arrRoadNodes[nn].m_nCard == pn1.m_nCard
			)
		{
			// 与下一点卡号相同，则用下一点的ID号和路号
			pn1.m_nID = pn0.m_nID + 1;
			pn1.m_nRoad = m_arrRoadNodes[nn].m_nRoad;
			pn1.m_fRoadRad = m_arrRoadNodes[nn].m_fRoadRad;
		}
		else
			ASSERT(0);	// ?? 异常处理

		// 重新计算，因为插入的话，第二个点的路号不对，后面计算方向不对
		// 以第二点为起点，不行，会走另外一条路
		//int nStart = pCar->m_arrPath[1].m_nID - 1;
		//// 重新计算路径	// ?? 有可能是另外一条路线
		//aStar.getPath(nStart, nEnd);
	}

	// 20220906 加这一段，发车时清除初始化
	// 潜伏式从中间存取货，开始运行再清初始化标志
#pragma 警告：如何转换初始化!!!
	//if (pCar->m_pReadData->sd.sd9.nCmd & 0x1000	// AGV初始化
	//	)
	//{
	//	// 复位初始化信号
	//	pCar->m_pReadData->sd.sd9.nCmd &= ~0x1000;
	//	//pCar->m_pSocket->sendData1(pCar->m_pReadData->sd.sd9.nCmd);
	//}

	// 开始行走
	pCar->setPath(aStar.getPath(), nEnd);		// 可能改变pCar->m_arrPath.m_nSize;

	// setPath中重新计算了m_nSendIndex，这里再次清零
	pCar->m_nSendIndex = 0;

	// 开始行走
	RoadNode* prn = &m_arrRoadNodes[nEnd];
	pCar->setEndNode(prn, m_arrEndPoint[(INT_PTR)prn->m_nPointID - 1]);

	return 0;
}*/

/// <作者>Mr. Hu</作者>
/// <日期>20210204</日期>
/// <功能>存货</功能>
/// <参数 name = "pCar">指定的AGV</参数>
/// <返回>0 成功，其它不成功</返回>
/// <说明>完成取货工作，寻找存货点后存货，用PLC控制活动行。返回0，正常，<0不正常</说明>
int Ground::carStartPut( Car * pCar )
{
	// 指定存放点
	if (pCar->m_pEndPoint
		&& pCar->m_pEndPoint->isSpecify()		// 如果是指存货定点
		)
	{
		// 指定存货点时，第二个参数是存货点序号 + 1，避开0
		return carStartPut(pCar, pCar->m_bLoaded - 1);
	}

	// 20210420 加个锁，否则两台AGV同时存货
	HLock hl(&m_mutexStore, -1, 0);
	if (!hl.allowed())
		return 10;	// 锁不住跳过

	//int result = 0;
	//RoadNode * prn = pCar->getNode();	// 一定在端点，节点是端点的下一点。
	//ASSERT( prn );
	//int nStart = prn->m_nID - 1 + 1;	// 节点是端点的下一点。

	//HLock hl(&pCar->m_mutexPath, -1, (DWORD)-29);	// 20200529 savePath中有锁，这里不用锁，这里加锁把存数据库也锁了，造成存取货AGV间断影响避让计算

	//ASSERT(pCar->m_arrPath.GetCount() == 2);	// 只能是端点
	//RoadNode* prn = &pCar->m_arrPath[1];
	//int nStart = prn->m_nID - 1;	// 变成0开始的序号。
	size_t len = pCar->m_arrPath.GetCount();
	ASSERT(len >= 2);	// 20201102 改在>=2，因为在线充电完成后路径多
	RoadNode* prn = &pCar->m_arrPath[len - 1];
	int nStart = prn->m_nID - 1;	// 变成0开始的序号。

	// 一定是存货 
	ASSERT( pCar->m_bLoaded );	// AGV有货

	// 没有第二个终点,表示存贷结束，找取货点
	int id = -1;
	if (theApp.m_bTransverse)
		id = matchStore3(nStart, pCar->m_bChangeColor);
	else if (theApp.m_bPlcCtrl && !theApp.m_bOnlyCall)		// PLC控制存储区
		id = matchStore(pCar);
	else
		id = matchStore2(pCar, nStart);	// 智能判断存储区

	if( id < 0 )
		return 11;	// 没有匹配的取货点或存货点

	// ?? 本线程中计算，可能不用同步
	//m_pAStar->getPath( nStart, id );
	//pCar->setPath(m_pAStar->getPath());
	AStar aStar(m_arrRoadNodes);
	aStar.getPath(nStart, id);

	// 20210323 如果是继续向前行驶，把原路径的到数第二点加到新路径的第一点
	// 20220219 以下适用于从道路中间改道，只有发车用
	INT_PTR len1 = pCar->m_arrPath.GetCount();
	if (len1 > 1)
	{
		ASSERT(aStar.m_arrPath.GetCount() > 1);
		// 如果不是回头路，把上一次路径的倒数第二个点加新路径的第一点。
		if (
			//(pCar->m_pReadData->nState == 0x1025)								// 20220219 只有发车用
			(len1 == 2)								// 20220219 只有发车用
			&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[1].m_nCard)
			&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[0].m_nCard)		// 排除发车时反向行行驶
			)
		{
			// ?? 没有计算m_G和m_F
			aStar.m_arrPath.InsertAt(0, pCar->m_arrPath[0]);
		}
	}

	pCar->setPath(aStar.getPath(), id);		// 可能改变pCar->m_arrPath.m_nSize;

	// 开始行走
	prn = &m_arrRoadNodes[id];
	pCar->setEndNode( prn, m_arrEndPoint[(INT_PTR)prn->m_nPointID - 1] );

	// 设置AGV对应的呼叫点通讯实例
	// 20201212 改到setEndNode里面
	//prn = &m_arrRoadNodes[0];
	//if (prn->m_nPointID)
	//	pCar->m_pCallSocket = m_arrEndPoint[(INT_PTR)prn->m_nPointID - 1]->m_pSocket;

	return 0;
}

// 结束通讯线程
void Ground::killThread()
{
	for(INT_PTR i = m_arrCars.GetCount() - 1; i >= 0; i-- )
	{
		//HLock hl(&m_arrCars[i]->m_mutexPath, 1, -30);	// 等待读，确保写数据完成，退出时自动解锁

		m_arrCars[i]->killThread();
	}

	for(INT_PTR i = m_arrPointSocket.GetCount() - 1; i >= 0; i-- )
	{
		m_arrPointSocket[i]->killThread();
	}

	m_socketWcs.killThread();
}

// 设置活动库区，同时设置左右工位，清除其它工位
/*void Ground::setStoreActived(EndPoint * pStorePoint)
{
	//CString s1 = pStorePoint->getProductNum();	// 左右两个工位编号相同
	INT_PTR n = m_arrEndPoint.GetCount();
	for (int i = 0; i < n; i++)
	{
		EndPoint * pep1 = m_arrEndPoint[i];
		if (pStorePoint->isSameLine(pep1))	// 生产线号想同
		{
			ASSERT(pStorePoint->isStore());

			pep1->m_bPointActived = pep1->isNeighbor(pStorePoint);		// 库区号，左右两个工位相同，取货点是0
		}
	}

	for (int i = 0; i < n; i++)
	{
		EndPoint * pep1 = m_arrEndPoint[i];
		if (pStorePoint->isParent(pep1) && pep1->m_pSocket)	// 找到对应的取货点 //?? 异常处理
		{
			WORD na = 1 << pStorePoint->m_nSerial;		// ?? 有的可能没有活动行，变成可选项
			((ProductPoint*)pep1)->m_pSocket->m_nActived = na;	// H9.00-9.03 活动行号
			((ProductPoint*)pep1)->m_pSocket->sendData(9, &na, 1);	// H9.00-9.03 活动行号
			break;
		}
	}

	//layerout();		// 重生成背景
	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错

	// ?? 找不到，异常处理
}
*/

// 设置活动库区，同时设置左右工位，清除其它工位
/*void Ground::setStoreActive(EndPoint * pStorePoint)
{
	INT_PTR n = m_arrEndPoint.GetCount();
	for (int i = 0; i < n; i++)
	{
		EndPoint * pep1 = m_arrEndPoint[i];
		if (pStorePoint->isSameLine(pep1))	// 生产线号想同
		{
			ASSERT(pStorePoint->isStore());

			pep1->m_bPointActived = pep1->isNeighbor(pStorePoint);		// 库区号，左右两个工位相同，取货点是0
		}
	}

	for (int i = 0; i < n; i++)
	{
		EndPoint * pep1 = m_arrEndPoint[i];
		if (pStorePoint->isParent(pep1) && pep1->m_pSocket)	// 找到对应的取货点 //?? 异常处理
		{
			WORD na = 1 << pStorePoint->m_nSerial;		// ?? 有的可能没有活动行，变成可选项
			((ProductPoint*)pep1)->m_pSocket->m_nActived = na;	// H9.00-9.03 活动行号
			((ProductPoint*)pep1)->m_pSocket->sendData(9, &na, 1);	// H9.00-9.03 活动行号
			break;
		}
	}

	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错

	// ?? 找不到，异常处理
}
*/




// 从下位机设置二次取货行，同时设置左右工位，清除其它工位
// 在线程循环中执行，要求程序简短
/*void Ground::setGet2(PointSocket * pSocket)
{
	ASSERT(pSocket);

	// 取下位机数据
	PWORD pp = pSocket->getPlcData();
	WORD na = pp[4];	// H4.00-H4.03	当前活动行	上下位机互传	上下位机都可能进行换行操作，最终结果以上位机下传为准
	int nActive = -1;	// 活动库区，数字0-15	// 清远家美专用，二次取货点4和5
	if (pSocket->m_bEnable2 && na)
	{
		nActive = 0;	// 活动库区，数字0-15	// 清远家美专用，二次取货点4和5
		for (; !(na & 1); nActive++, na >>= 1);	// 把二进制位转换为数字，最多16个
	}

	// 设置同一线号的活动垛
	INT_PTR n = m_arrEndPoint.GetCount();
	if (!n)
		return;

	for (int j = 0; j < n; j++)
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		if (pep2->m_bufCode[0] == L'2')	// 清远专用，2xxx为二次取货区
		{
#pragma aaaaaa("!!!!!!")
			//			pep2->m_bGet2 = (pep2->m_nSerial == nActive);	// 判断同一生产线的两个工位号
		}
	}

	//layerout();	// 重画背景
	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错
}


// 从下位机设置二次存货行，同时设置左右工位，清除其它工位
// 在线程循环中执行，要求程序简短
void Ground::setStore2(PointSocket * pSocket)
{
	ASSERT(pSocket);

	// 取下位机数据
	PWORD pp = pSocket->getPlcData();
	WORD na = pp[6];	// H6.00-H6.03	当前活动行	上下位机互传	上下位机都可能进行换行操作，最终结果以上位机下传为准
	int nActive = -1;	// 活动库区，数字0-3
	if (pSocket->m_bEnable2 && na)
	{
		nActive = 0;	// 活动库区，数字0-3
		for (; !(na & 1); nActive++, na >>= 1);	// 把二进制位转换为数字，最多16个
	}

	// 设置同一线号的活动垛
	INT_PTR n = m_arrEndPoint.GetCount();
	if (!n)
		return;

	for (int j = 0; j < n; j++)
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		if (pep2->m_bufCode[0] == L'3')	// 清远专用，3xxx为二次取货区
		{
			pep2->m_bStore2 = (pep2->m_nSerial == nActive);	// 判断同一生产线的两个工位号
		}
	}

	//layerout();	// 重画背景
	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错
}
*/

// 从下位机设置二次取货行，同时设置左右工位，清除其它工位
// 在线程循环中执行，要求程序简短
void Ground::setStoreActive2(PointSocket * /*pSocket*/)
{
	/*
	ASSERT(pSocket);

	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错

	// 设置同一线号的活动垛
	INT_PTR n = m_arrEndPoint.GetCount();
	if (!n)
		return;

	for (INT_PTR j = 0; j < n; j++)
	{
		EndPoint* pep2 = m_arrEndPoint[j];
		if (pep2->m_pSocket != pSocket)
			continue;	// 不是指定的呼叫点，跳过

		// 设置对应的存货点
		for (INT_PTR i = 0; i < n; i++)
		{
			EndPoint* pep3 = m_arrEndPoint[i];
			if (!pep3->isParent(pep2))
				continue;	// 如果pep3不是子节点，跳过
			if (pep3->m_bufCode[0] != L'2')	// 清远专用，2xxx为二次取货区
				continue;

			if (!pSocket->m_bEnable2 || pep3->m_nSerial != pSocket->m_nGet2)
			{
				pep3->setNextPrevious2(0);		// 不是二次取货行
				pep3->m_pPreviousPoint2 = 0;	// 没有上级
				continue;
			}

			pep3->m_pPreviousPoint2 = pep2;

			// 是二次取货行，设置存货点
			for (INT_PTR ii = 0; ii < n; ii++)
			{
				EndPoint* pep4 = m_arrEndPoint[ii];
				if (pep4->m_nSerial != pSocket->m_nStore2)
					continue;	// 序号不同，跳过
				if (!pep4->isParent(pep3))
					continue;	// 如果pep3不是子节点，跳过

				pep3->setNextPrevious2(pep4);
			}
		}
	}
	*/
}

// 设置/启用取货点，同时设置左右工位
// 活动工位被其它呼叫点占用时，返回1，否则返回0
void Ground::setProductDisabled( PointSocket * /*pSocket*/, BOOL /*bDisable*/ )
{
	/*
	INT_PTR n = m_arrEndPoint.GetCount();
	for( int i = 0; i < n; i++ )
	{
		EndPoint * pep1 = m_arrEndPoint[i];
		if( pep1->m_pSocket == pSocket )
			pep1->m_bDisabled = bDisable;
	}

	int nr = setStoreActive(pSocket);
	if (nr == 3 && !bDisable)
	{
		pSocket->setStoreActive(-1);	// 清除活动行，主要用于停用后活动行被其它呼叫点占用的情况
	}

	//layerout();		// 重新画背景，显示停用
	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错
	*/
}

// 设置/启用取货点，同时设置左右工位
void Ground::setProductDisabled( EndPoint * pEndPoint )
{
	INT_PTR n = m_arrEndPoint.GetCount();
	BOOL b = pEndPoint->m_bDisabled;
	//PointSocket * pSocket = 0;	// 设置PointSocket停用，并下传到PLC中
	for( int i = 0; i < n; i++ )
	{
		EndPoint * pep1 = m_arrEndPoint[i];
		//ASSERT(pEndPoint->isStore());	// 20190214 这里是产品取货点，不知道为什么加这一条
		if( pep1->isNeighbor(pEndPoint) )		// 库区号，左右两个工位相同
		{
			pep1->m_bDisabled = b;

			111111111111;
			//if( !pSocket			// 通讯类只设置一次
			//	&& pep1->m_pSocket	// 有通讯类
			//)
			//{
			//	pSocket = pep1->m_pSocket;
			//	pSocket->m_bDisabled = b;
			//	//WORD v = b ? 1 : 0;
			//	//pSocket->sendBitByBit( 8, 0x0004, v );	// 停用标志发到PLC
			//}
		}
	}
}

// 唯美打包机取货专用，变色号，换色号，存货区空一垛，重新开始。
void Ground::changeProductColor( EndPoint * pStorePoint )
{
	// 取两个工位中垛数较多的较大的一个
	int x = 0;
	for(INT_PTR i = m_arrEndPoint.GetCount() - 1; i >= 0; i-- )
	{
		EndPoint * pep = m_arrEndPoint[i];
		ASSERT(pStorePoint->isStore());
		if( !pep->isNeighbor(pStorePoint) )	// 不是同类，跳过
			continue;

		int x1 = pep->getXCurrent();
		if( x1 > x )
			x = x1;
	}

	if( x == 0 )	// 取开始点，不空格
		return;

	// 变换相邻的库区
	x++;	// 空一格
	for(INT_PTR i = m_arrEndPoint.GetCount() - 1; i >= 0; i-- )
	{
		EndPoint * pep = m_arrEndPoint[i];
		ASSERT(pStorePoint->isStore());
		if( !pep->isNeighbor(pStorePoint) )	// 不是同类，跳过
			continue;

		pep->setXCurrent( x );
		pep->setZCurrent( pep->getZNumber() );	// 填充，存下一垛实现空格效果
		((StorePoint*)pep)->save();

		// 修改相邻端点信息	// ?? 重启软件有问题
		//pep->m_strProductModel = pStorePoint->m_strProductModel;
		//pep->m_strProductNumber = pStorePoint->m_strProductNumber;
		//pep->m_strProductGrade = pStorePoint->m_strProductGrade;
	}
}


// 20170511 到中间点后，如果有货，延长路径
// Car * pCar 指定AGV
void Ground::gotoProduct( Car * pCar )
{
	HLock hl(&pCar->m_mutexPath, -1, (DWORD)-31);	// 等待写，退出时自动解锁

	INT_PTR nPath = pCar->m_arrPath.GetCount();
	int n = pCar->m_arrPath[nPath-1].m_nID - 1;
	RoadNode * prn = &m_arrRoadNodes.GetAt( n );

	if( prn->m_nCard < 0 )	// 不是两段导航的中间点
		return;

	// 如果有货，延长导航
	int nGet = matchProduct2( prn->m_nCard );
	if( nGet < 0 ) // 无货
		return;

	int nStart = pCar->m_arrPath[0].m_nID - 1;
	pCar->removePath(0, pCar->m_arrPath.GetCount());	// 用RemoveAt清空数组，保留内存
	//pCar->removeCar();	// 移除当前AGV占用的所有节点

	//m_pAStar->getPath( nStart, nGet );	// ?? 复杂路径加优化导航时可能与当前路径不一致
	//pCar->setPath(m_pAStar->getPath());
	AStar aStar(m_arrRoadNodes);
	aStar.getPath(nStart, nGet);

	// 20210323 如果是继续向前行驶，把原路径的到数第二点加到新路径的第一点
	// 20220219 以下适用于从道路中间改道，只有发车用
	INT_PTR len1 = pCar->m_arrPath.GetCount();
	if (len1 > 1)
	{
		ASSERT(aStar.m_arrPath.GetCount() > 1);
		// 如果不是回头路，把上一次路径的倒数第二个点加新路径的第一点。
		if (
			//(pCar->m_pReadData->nState == 0x1025)								// 20220219 只有发车用
			(len1 == 2)								// 20220219 只有发车用
			&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[1].m_nCard)
			&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[0].m_nCard)		// 排除发车时反向行行驶
			)
		{
			// ?? 没有计算m_G和m_F
			aStar.m_arrPath.InsertAt(0, pCar->m_arrPath[0]);
		}
	}

	pCar->setPath(aStar.getPath(), nGet);		// 可能改变pCar->m_arrPath.m_nSize;

	prn = &m_arrRoadNodes[nGet];
	pCar->setEndNode( prn, m_arrEndPoint[(INT_PTR)prn->m_nPointID - 1] );
	//if(pCar->m_pReadData->nState != 0x1025)	// 20190826 新加条件，始发时不减
	//	pCar->m_nSendIndex--;	// 重发衔接处的数据，原来没有转弯信息
}

// 20170512 取中间点的等待距离
// int nCard 中间点卡号
// 返回正数或0，有等待距离，否则不是中间点
int Ground::getMiddleDis( int nCard )
{
	if( nCard < 0 )	// 不是两段导航的中间点	//?? 导常
		return 0;

	// 否则行驶到指定距离
	INT_PTR n2 = m_arrEndPoint.GetCount() - 1;
	for( ; n2 >= 0; n2-- )
	{
		EndPoint * pep = m_arrEndPoint[n2];
		if( pep->m_nMiddleID > 0 && m_arrRoadNodes[(INT_PTR)pep->m_nMiddleID-1].m_nCard == nCard )	// 中间点卡号相同
		{
			return pep->m_nMiddleDis;
		}
	}

	return 0;	//?? 异常
}

// 从下位机设置活动库区，同时设置左右工位，清除其它工位
// 在线程循环中执行，要求程序简短
// 返回0 成功，1 没有端点表，2 没有对应端点，3 端点被占用
int Ground::setStoreActive( PointSocket * pSocket )
{
	ASSERT(pSocket);

	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错

	// 设置同一线号的活动垛
	INT_PTR n = m_arrEndPoint.GetCount();
	if (!n)
		return 1;	// 没有端点表

	if (pSocket->m_bDisabled)	// 停用
	{	// 查找激活点
		for (INT_PTR j = 0; j < n; j++)
		{
			EndPoint* pep2 = m_arrEndPoint[j];
			//if (pep2->m_pSocket != pSocket)
			//	continue;	// 不是指定的呼叫点，跳过
			pep2->setNextPrevious(0);
		}
		return 0;
	}

	// 激活
	EndPoint* pep = 0;
	EndPoint* pep3 = 0;

	// 查找激活点
	for (INT_PTR j = 0; j < n; j++)
	{
		EndPoint* pep2 = m_arrEndPoint[j];
		//if (pep2->m_pSocket != pSocket)
		//	continue;	// 不是指定的呼叫点，跳过

		// 设置对应的存货点
		for (INT_PTR i = 0; i < n; i++)
		{
			pep3 = m_arrEndPoint[i];
			if (pep3->m_nSerial != pSocket->m_nActived)
				continue;	// 序号不同，跳过
			if (!pep3->isParent(pep2))
				continue;	// 如果pep3不是子节点，跳过

			pep = pep2;	// 激活点
			break;
		}

		if (!pep)
			return 2;	// 没有激活点

		// 激活点是否被占用
		for (INT_PTR jj = 0; jj < n; jj++)
		{
			EndPoint* pep1 = m_arrEndPoint[jj];
			if (pep3 == pep1->getNextPoint())
				return 3;	// 端点被占用
		}
		pep->setNextPrevious(pep3);
	}

	return 0;	// 激活成功
}


/// <作者>Mr. Hu</作者>
/// <日期>20240904</日期>
/// <功能>设置活动存储区</功能>
/// <参数 name = "pEndPoint">指定的站点</参数>
/// <返回>1成功，0失败</返回>
/// <说明>同一类型的站点只有一个活动区</说明>
int Ground::setPointActive(EndPoint* pEndPoint)
{
	ASSERT(pEndPoint);

	if (!pEndPoint->isStore())
		return 0;

	INT_PTR n = m_arrEndPoint.GetCount();
	if (!n)
		return 0;	// 没有端点表

	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错

	// 复位原激活点
	bool bActive = 1;
	CString strsql;
	for (INT_PTR j = 0; j < n; j++)
	{
		EndPoint* pep2 = m_arrEndPoint[j];
		if (*(UINT64*)(LPCTSTR)pEndPoint->m_bufCode
			!= *(UINT64*)(LPCTSTR)pep2->m_bufCode
			)
			continue;					// 不是同一类，跳过
		if (!pep2->m_bActived)
			continue;					// 未激活，路过
		if (pep2 == pEndPoint)
		{
			bActive = 0;					// 已经激活，取消激活
			break;
		}

		pep2->m_bActived = 0;
		strsql.AppendFormat(L"UPDATE `point` SET `disable` = %i WHERE `id` = %i;", pep2->m_nPointState, pep2->m_nPointID);	// 复位原激活
	}

	pEndPoint->m_bActived = bActive;
	pEndPoint->m_bDisabled = 0;
	strsql.AppendFormat(L"UPDATE `point` SET `disable` = %i WHERE `id` = %i;", pEndPoint->m_nPointState, pEndPoint->m_nPointID);	// 设置新激活
	theApp.getDatabase()->Execute(strsql);

	return 1;	// 激活成功
}


/// <说明>设置pPointSocket对应的所有端点停用</说明>
/*void Ground::setStoreDisable(PointSocket* pSocket)
{
	ASSERT(pSocket);

	m_bDrawBackground = TRUE;	// 重画背景，在线程中执行，效率低，还可能同时执行而出错

	// 设置同一线号的活动垛
	INT_PTR n = m_arrEndPoint.GetCount();
	if (!n)
		return;	// 没有端点表

	// 查找激活点
	for (INT_PTR j = 0; j < n; j++)
	{
		EndPoint* pep2 = m_arrEndPoint[j];
		if (pep2->m_pSocket != pSocket)
			continue;	// 不是指定的呼叫点，跳过
		pep2->setNextPrevious(0);
	}
}
*/

// 把库区垛数发送到PLC
// pStorePoint 可能是产品点
void Ground::downPile( EndPoint * pStorePoint )
{
	if (theApp.m_bOnlyCall)
		return;		// ?? 库区较多时设托数

	//CString & s1 = pStorePoint->getProductNum();	// 左右两个工位编号相同
	INT_PTR n = m_arrEndPoint.GetCount();

	// ??唯美专用 库区垛数据，唯美每个库区4大行，8小行，2个小行对应一条生产线，最多5大行，10小行。
	// 每两个字节表示一小行，第一个字节是垛数，第二个字节是这一小行最后一垛的层数
	//BYTE data[32];	// 20180817 江西唯美，最多16对
	BYTE data[64];	// 20200824 重庆唯美，改为64
	memset( data, 0, sizeof(data) );

	/*
	// 遍历所有端点
	for( int i = 0; i < n; i++ )
	{
		EndPoint * pep1 = m_arrEndPoint[i];
		//CString & s2 = pep1->getProductNum();
		if (!pStorePoint->isSameLine(pep1))	// 不同线号，不是存储区，跳过
			continue;

		if(pep1->m_pSocket)		// 取货点
		{
			//ASSERT( pep1->IsKindOf( RUNTIME_CLASS(ProductPoint)) );	// 产品类
			//ASSERT( pep1->m_pSocket );
			pSocket = pep1->m_pSocket;
		}
		else
		{
			//int nn = pep1->getType2() * 2 + pep1->m_nSerial * 4;			// ?? 唯美专用、
			//int nn = pep1->getPLCAdd();
			// ?? 清远家美专用，端点代码后两位是序号，从01开始，必须连续
			int nn = (pep1->m_bufCode[2] - L'0') * 10	// 十位转数字
				+ (pep1->m_bufCode[3] - L'0');			// 个位转数字
			nn = (nn - 1) * 2;	// 变成从0开始
			nn += (pep1->m_nSerial * 4);
			ASSERT( nn < (sizeof( data ) - 1) );	// ?? 异常处理

			data[nn + 1] = (BYTE)pep1->getXCurrent();	// 垛数
			data[nn] = (BYTE)pep1->getZCurrent();	// 层数
		}
	}

	//ASSERT( pSocket );
	if (pSocket)	// ?? 异常处理
	{
		// 上位机设置本条线对应的所有垛数，十小行，20个字节	// 20180817 江西唯美，最多16对
		pSocket->sendData(10, (PWORD)data, sizeof(data) / 2);
	}
	*/

	// 20200716 适应共享可能要改多个呼叫点，两次循环
	// 遍历所有端点，修改存储区
	for (int i = 0; i < n; i++)
	{
		EndPoint* pep1 = m_arrEndPoint[i];
		//CString & s2 = pep1->getProductNum();
		if (!pStorePoint->isSameLine(pep1))	// 不同线号，不是存储区，跳过
			continue;

	//	if (!pep1->m_pSocket)		// 存货点
	//	{
	//		//int nn = pep1->getType2() * 2 + pep1->m_nSerial * 4;			// ?? 唯美专用、
	//		//int nn = pep1->getPLCAdd();
	//		// ?? 清远家美专用，端点代码后两位是序号，从01开始，必须连续
	//		int nn = (pep1->m_bufCode[2] - L'0') * 10	// 十位转数字
	//			+ (pep1->m_bufCode[3] - L'0');			// 个位转数字
	//		nn = (nn - 1) * 2;	// 变成从0开始
	//		nn += (pep1->m_nSerial * 4);
	//		ASSERT(nn < (sizeof(data) - 1));	// ?? 异常处理

	//		data[nn + 1] = (BYTE)pep1->getXCurrent();	// 垛数
	//		data[nn] = (BYTE)pep1->getZCurrent();	// 层数
	//	}
	}

	// 如果是产品点，直接下发
	//if (pStorePoint->m_pSocket)
	//{
	//	pStorePoint->m_pSocket->sendData(10, (PWORD)data, sizeof(data) / 2);
	//}

	// 遍历所有端点，修改呼叫点 // ?? 不连续时同一数据传多
	//PointSocket* pSocket = 0;
	//for (int i = 0; i < n; i++)
	//{
	//	EndPoint* pep1 = m_arrEndPoint[i];

	//	if (pep1->m_pSocket && pStorePoint->isParent(pep1))		// 存货点
	//	{
	//		if (pSocket != pep1->m_pSocket) // ?? 这个条件判断，不连续时同一数据传多次
	//		{
	//			pSocket = pep1->m_pSocket;
	//			pSocket->sendData(10, (PWORD)data, sizeof(data) / 2);
	//		}
	//	}
	//}
}

// 加减活动行的托数
// PointSocket * pSocket 取货点通讯类, int nLeftRight 0左 1右, int nV 只有两个值 -1 +1
// ?? 加减不成功的处理
/*void Ground::setIncDec( PointSocket * pSocket, int nLeftRight, int nV )
{
	// 查找匹配通讯的端点
	EndPoint * pep11 = matchProduct( pSocket );
	ASSERT( pep11 );	// ?? 异常处理

	// 设置同一线号的活动垛
	INT_PTR n = m_arrEndPoint.GetCount();
	//CString & s1 = pep11->getProductNum();
	for( int j = 0; j < n; j++ )
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		//CString & s2 = pep2->getProductNum();
		if( pep11->isSameLine(pep2)		// 生产线号相同
			&& pep2->isStore()							// 存储区
			&& (pep2->m_bufCode[3] - L'1') == nLeftRight	// 工位号相同	// ?? 只能是一位，可以用字符
#pragma aaaaaa("!!!!!!")
//			&& pep2->m_bPointActived					// 激活
			)
		{
			((StorePoint*)pep2)->setIncDec( nV );
		}
	}
}*/

// 加减活动行的托数
// PointSocket * pSocket 取货点通讯类, int nLeftRight 0左 1右, int nV 只有两个值 -1 +1
// ?? 加减不成功的处理
void Ground::setIncDec( PointSocket * pSocket, int /*nLeftRight*/, int /*nV*/ )
{
	if (pSocket->m_bDisabled)	// 停用
		return;

	/*
	// 设置同一线号的活动垛
	INT_PTR n = m_arrEndPoint.GetCount();
	//CString & s1 = pep11->getProductNum();
	for( int j = 0; j < n; j++ )
	{
		EndPoint * pep = m_arrEndPoint[j];
		if (pep->m_pSocket != pSocket)
			continue;
		EndPoint* pep2 = pep->getNextPoint();

		//if (!pep2)
		//	return;	// 停用时，子节点是0;

		ASSERT(pep2);
		ASSERT(pep2->isStore());

		if( (pep2->m_bufCode[3] - L'1') == nLeftRight)	// 工位号相同	// ?? 只能是一位，可以用字符，不严谨
		{
			((StorePoint*)pep2)->setIncDec( nV );
		}
	}
	*/
}

// 下位机清一行
void Ground::clearLine( PointSocket * pSocket )
{
	// 查找匹配通讯的端点
	EndPoint * pep11 = matchProduct( pSocket );
	ASSERT( pep11 );	// ?? 异常处理

	// 行号
	PWORD pp = pSocket->getPlcData();
	ASSERT(pp[2]);	// ?? 异常处理
	WORD nn1 = pp[2];		// H20.00 - H20.03 要清空的行号
									// 20180817 江西唯美，最多16对 改到 H2.00 - H2.15
	int ch = 0;	// 行号
	for( ; (nn1 & 1) == 0; ch++, nn1 >>= 1 );	// 二进制位转换成数字



	// ???? 没有锁定


	
	INT_PTR n = m_arrEndPoint.GetCount();


	// 20200716 适应共享可能要改多个呼叫点，用父子关系，// ?? 只适用于共享所有道，共享一部分时不适用
	// 先找匹配的存储行
	for (int j = 0; j < n; j++)
	{
		EndPoint* pep2 = m_arrEndPoint[j];
		if (pep2->isParent(pep11)		// 生产线号相同
			&& pep2->isStore()
			&& pep2->m_nSerial == ch		// 行号相同
			)
		{
			pep11 = pep2;
			break;
		}
	}












	// 清除同一线号
	for( int j = 0; j < n; j++ )
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		//CString & s2 = pep2->getProductNum();
		if( pep11->isSameLine(pep2)		// 生产线号相同
			&& pep2->isStore()
			&& pep2->m_nSerial == ch		// 行号相同
			)
		{
			pep2->removeAll(-2);
			if (theApp.m_bPlcCtrl)		// PLC控制垛数，库区行数小于10时适用
				downPile( pep2 );
		}
	}
}

// 入库，同一生产线入库，直接换行，如果换行不成功，不清入库标识
void Ground::stored( PointSocket * pSocket )
{
	if (pSocket->m_bDisabled)
		return;	// 停用

	// 查找匹配通讯的端点
	EndPoint * pep11 = matchProduct( pSocket );
	ASSERT( pep11 );	// ?? 异常处理

	//if( changeLine( pep11 ) > 0 )
	//	pSocket->sendBitByBit( 8, 0x0100, 0 );	// 复位下位机，h8.08
}

// 抽检，左右只放一托
void Ground::randomCheck( PointSocket * /*pSocket*/ )
{
	//// 查找匹配通讯的端点
	//INT_PTR n = m_arrEndPoint.GetCount();
	//for( int ii = 0; ii < n; ii++ )
	//{
	//	EndPoint * pep11 = m_arrEndPoint[ii];
	//	if( pep11->m_pSocket == pSocket )	// 匹配通讯口
	//	{
	//		pep11->m_bRandomCheck = TRUE;
	//	}
	//}
}

// 转到相邻的工位
BOOL Ground::gotoNeighbor( Car * /*pCar*/ )
{
	return FALSE;
	111111111111;
	/*
	ProductPoint * pPoint = (ProductPoint*)(pCar->m_pEndPoint);
	PointSocket * pSocket = pPoint->m_pSocket;

	ASSERT(pSocket);

	// 查找匹配通讯的端点
	EndPoint * pep11 = 0;
	INT_PTR n = m_arrEndPoint.GetCount();
	int ii = 0;
	for( ; ii < n; ii++ )
	{
		pep11 = m_arrEndPoint[ii];
		if( pep11->m_pSocket == pSocket	// 匹配通讯口
			&& pep11 != pPoint	// 排除当前工位
			)
		{
			//pep11->getLength( pCar->m_bLoaded);	// 借用这个函数计算正在工作的垛，?? 重启后如果有新垛可能计算不准确
			BOOL 呼叫 = (((ProductPoint*)pep11)->m_nPile != 0);
			if (!呼叫)
				continue;	// 20201209 大将军多个工位，都判断
				//return FALSE;
			else
				break;
		}
	}
	if (ii == n)
		return FALSE;

	// 导航到相邻工位
	// 计算路径
	int nStart = pPoint->getRoadNode()->m_nID - 1;
	int nGet = pep11->getRoadNode()->m_nID - 1;
	//m_pAStar->getPath( nStart, nGet );
	//pCar->setPath(m_pAStar->getPath());
	AStar aStar(m_arrRoadNodes);
	aStar.getPath(nStart, nGet);
	pCar->setPath(aStar.getPath(), nGet);		// 可能改变pCar->m_arrPath.m_nSize;

	// 开始行走
	RoadNode & rn = m_arrRoadNodes[nGet];
	pCar->setEndNode( &rn, m_arrEndPoint[(INT_PTR)rn.m_nPointID - 1] );
	// pCar->lockNode( &rn, pCar );	// 20170422 唯美打包机出货点，锁定终点

	return TRUE;
	*/
}

//与存货AGV路径相同，方向相反，到旁边避让，双复线时主要用于在端点等待时另一个AGV过来了
// 如果避让到另一边，返回另一点的序号(ID-1)，否则返回-1，不避让
/*
int Ground::gotoOther( Car * pCar, int nStart)
{
	BOOL bSame = FALSE;
	INT_PTR len = pCar->m_arrPath.GetCount();
	CArray<PathNode, PathNode&> & path1 = pCar->m_arrPath;
	for(INT_PTR n = m_arrCars.GetCount() - 1; n >= 0 && !bSame; n-- )
	{
		Car * pCar2 = m_arrCars[n];
		if( pCar2->getDisable() )
			continue;	// 停用，跳过

		if( pCar == pCar2 )
			continue;	// 是自己，跳过

		CArray<PathNode, PathNode&> & path2 = pCar2->m_arrPath;
		INT_PTR len2 = path2.GetCount();
		if(len2 <= 1 )
			continue;	// 如果路径小于等于1，没有运行，跳过

		// 在端点等待或始发车，另一台AGV要进来，转到相邻工位
		// ?? 在这个点发车，没有取货信号，里面还有AGV，可能出来撞上
		if (pCar->waitGet() || pCar->isInit())	// 本AGV在端点
		{
			int nCard = m_arrRoadNodes[nStart].m_nCard;		// 等待时，路径有两个点，第0个是进入点，第1个是端点。
			bSame = !(pCar2->m_nRunIndex == len2 - 2 && pCar2->m_pEndNode)	// 发车时可能两个同时在端点，不到相邻工位 
				&& nCard == path2[len2-1].m_nCard;
			if (bSame)
				break;
			else
				continue;
		}

		// 第一点相同
		int nRunIndex2 = pCar2->m_nRunIndex;
		INT_PTR j = len2 - 1;
		int i = 0;
		if (path1[i++].m_nCard != path2[j--].m_nCard)
			continue;

		// 比较路径	// ?? 不全面
		bSame = TRUE;
		for( ; i < len && j >= nRunIndex2; i++, j-- )
		{
			if (path1[i].m_nCard == path2[j].m_nCard)	// 如果相同，继续
			{
				//if( m_arrRoadNodes[path1[i].m_nID - 1].m_pCar == pCar2 )	// 从路径点返道路点
				//	break;	// 如果道路点被同路径的AGV占用，避让
				//else
					continue;
			}

			// 20171104 如果分开一段又合上，分开的距离小于扩大的安全距离，算是重合路段，否则可能在这样的路上卡住
			PathNode & pn = path2[j];
			INT_PTR jj = j - 1;
			int ii = 0;
			BOOL bJoin = FALSE;
			int nSafeDis = theApp.m_nExtSafe;	// 扩大的安全距离
			for (; jj > 0; jj--)			// 查找相同的点		// ?? 多重循环效率低
			{
				PathNode & pn2 = path2.GetAt(jj);
				int dn = pn2.calDis(pn);	// 求两点间的折线距离	// 20180102改为直线距离
				if (dn > nSafeDis)		// 大于安全距离，结束
				{
					bSame = FALSE;	// 不避让
					break;
				}

				ii = i + 1;
				for (; ii < len; ii++)	// 另一AGV相同点
				{
					if (path1[ii].m_nCard == pn2.m_nCard)
					{
						bJoin = TRUE;
						break;
					}
				}
				if (bJoin)
					break;
			}

			if (!bJoin)	// 指定的距离内没有交点
			{
				bSame = FALSE;	// 不避让
				break;	// 结束循环
			}
			else if (jj < nRunIndex2)	// 有一小段分开，对面AGV在分开路段
			{
				break;		// 避让
			}
			else
			{
				// 否则把小段分开视为相同路段，继续。
				j = jj + 1;
				i = ii - 1;
			}
		}

		// 如果断头路的最后一点在对方AGV的路线上，较远的AGV避让，因为第二段路线会与对方路线重合，
		// ?? 对方较远时可能不干涉
		//if( k + 1 == len1 - 1 && pArrPath1->GetAt(k+1) == path2.GetAt(j-1) ) 
		//{
		//	pCar->m_pAvoidCar = pc;
		//	return bb ? 2 : 1;
		//}
	}

	if (!bSame)	// 没有对头AGV，返回
		return -1;

	int nPointID = m_arrRoadNodes[nStart].m_nPointID;
	if (nPointID <= 0)
		return -1;

	EndPoint * pep = m_arrEndPoint[(INT_PTR)nPointID-1];

	INT_PTR n = m_arrEndPoint.GetCount();
	for( int i = 0; i < n; i++ )
	{
		EndPoint * pep3 = m_arrEndPoint[i];
		ASSERT(pep->isStore());
		if( pep3->isNeighbor( pep )	// 是同类
			&& pep != pep3 			// 不是自己
			&& !pointBeUsed(pep3)	// 没有被占用
			)
		{
			return pep3->getRoadNode()->m_nID - 1;
		}
	}

	// 没有可避让的路，?? 异常
	//ASSERT( FALSE );
	return -1;
}
*/

BOOL Ground::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
	BOOL result = HCtrl::OnWndMsg( message, wParam, lParam, pResult);
	if( message != WM_MOUSEWHEEL /*|| getWnd()->m_pctrMouseEnter != this */)	// getWnd()->m_pctrMouseEnter != this 不在本控件中
		return result;	// 不是滚轮，返回

	//int fwKeys = GET_KEYSTATE_WPARAM(wParam);		// 滚轮同时的按键
	int zDelta = GET_WHEEL_DELTA_WPARAM(wParam);	// 滚动量，120的倍数
	int xPos = GET_X_LPARAM(lParam);				// 鼠标位置，屏幕坐标
	int yPos = GET_Y_LPARAM(lParam); 
	
	// 计算比例
	float scale = zDelta > 0 ? 1.2f : 1 / 1.2f ;
	m_fWheelScale *= scale;
	if( m_fWheelScale < 1 )
		m_fWheelScale = 1;	// 最小1
	else if( m_fWheelScale > 50 )
		m_fWheelScale = 50;	// 最大10

	// 从屏幕坐标转换到控件坐标
	CRect rc( xPos, yPos, xPos, yPos );
	getWnd()->ScreenToClient( &rc );
	rcToClient( rc );
	m_fWheelX = float(rc.left);
	m_fWheelY = (float)rc.top;

	// 重画
	layerout();

	return TRUE;
}

// 20181025 下载托数，在线程循环中执行，代码尽量短
void Ground::synchronousStoreNumber( PointSocket * pSocket )
{
	ASSERT(theApp.m_bPlcCtrl);

	// 查找通讯类对应的取货点
	EndPoint * pep11 = matchProduct( pSocket );
	downPile(pep11);
}

// 取货，先找空的工位，再找最合适的AGV，一次找一个工位
// 工位优先级：有货/较远（排在数据库的后面）
// 按时间较短原则选取AGV，有可能选正在运行的AGV
// ?? 必须有中间点
/*void Ground::pickup()
{
	for (;;)
	{
		EndPoint * pep = getProduct();
		if (!pep)
			break;	// 没有空闲点，结束

		ASSERT(pep->m_nMiddleID);
		pep->m_nOccupy = 1;		// 设置为占用
		int nGet = pep->m_nMiddleID - 1;	// 中间点
		Car * pCar = getNearCar(nGet);	// 减1变成0开始的道路表中的序号
		if (pCar)
		{
			pCar->m_nOccupy = 1;
			if( pCar->waitGet() ) // 选择的AGV正在等待
				carStartGet(pCar, nGet);
		}
		else
			break;	// 没有空闲AGV，结束
	}

	// 端点变量清0
	for (INT_PTR i = m_arrEndPoint.GetCount() - 1; i >= 0; i--)
		m_arrEndPoint[i]->m_nOccupy = 0;
	// AGV变量清0
	for (INT_PTR i = m_arrCars.GetCount() - 1; i >= 0; i--)
		m_arrCars[i]->m_nOccupy = 0;
}*/

/// <作者>Mr. Hu</作者>
/// <日期>20201212</日期>
/// <功能>本区内取货优先</功能>
/// <参数 name = "pCar">请求的AGV</参数>
/// <返回>TRUE 取货成功，FALSE 没有取货</返回>
/// <说明>只计算Axxx，存空托完成后取实托</说明>
BOOL Ground::collPickup(Car* pCar)
{
	if (!pCar->waiting())
		return FALSE;	// 不是等待，不计算

	EndPoint* pep = pCar->m_pEndPoint;
	if (!pep)
		return FALSE;	// 没有终点，不计算

	// 20210106 改为A1区，电梯区不在此计算，因为要算层数够不够
	//if (pep->m_bufCode[0] != L'A')
	//	return FALSE;	// 不是Axxx区，不计算
	if (pep->getCode1() != *(int*)L"A1")
		return FALSE;	// 不是Axxx区，不计算

	// 从puckup()中复制过来
	INT32 dis = 0;
	EndPoint* pep2 = pep->m_pColl->getPickupPoint(pCar, dis);
	if (pep2)
	{
		int nGet = pep2->getRoadNode()->m_nID - 1;	// 如果是同类，直接导到终点
		if (carStartGet(pCar, nGet))
		{
			// 设置AGV对应的呼叫点通讯实例
			//pCar->m_pCallSocket = pep2->m_pSocket;
			return TRUE;
		}
	}
	
	return FALSE;
}

/// <作者>Mr. Hu</作者>
/// <日期>20201009</日期>
/// <功能>取货</功能>
/// <参数 name = "pCar">请求的AGV</参数>
/// <返回>1 成功，否则 0</返回>
/// <说明>不一定是请求的AGV取货，查找最合理的AGV取货，采用神经网络算法</说明>
bool Ground::pickup(Car* pCar)
{
	if (pCar->isLowBattery())
		return 0;

	ASSERT(pCar);
	HLock hl(&m_mutexPickup, -1, 0);	// 锁定运行锁，主要是防止找最近的AGV和这里冲突
	if (!hl.allowed())
		return 0;	// 锁不住跳过

	// Axxx区域内优先
	// 如果区内有空AGV，直接取本区内的货
	if (collPickup(pCar))
		return 1;

	// 计算集合优先级
	return m_colls.updatePriority(pCar);
}

// 20180129 按东鹏设计
// 取货，先找空的工位，再找最合适的AGV，一次找一个工位
// 工位优先级：有货/较远（排在数据库的后面）
// 按时间较短原则选取AGV，有可能选正在运行的AGV
// ?? 必须有中间点
// 如果pCar为0，再找最合适的AGV，否则启动pCar
/*void Ground::pickup( Car * pCar )
{
	ASSERT(pCar);
	//mutexPaoGuang.Lock();		// 线程互斥，计算去抛光线AGV时，只有一个线程工作。	//?? 未考虑锁定期间异常跳出，其它锁定线程也有这个问题

	HLock hl(&mutexProduct, -1, 0);	// 锁定取货点，// ?? 多AGV多取货点时效率问题
	if (!hl.allowed())		// 未锁定，跳过
		return;			// ?? 两个同时申请时可能把较近的跳过。

	for (;;)
	{
		EndPoint * pep = getProduct(pCar, 3);	// 然后到生产线工位
		if (!pep)
			pep = getProduct(pCar, 4);	// 最后到库区取货
		if (!pep)
		{
			if(pCar)
				carStartGet(pCar, -1);	// 20181019 第二个参数为-1时，尝试转到相邻工位避让
			break;	// 没有空闲点，结束
		}

		pep->m_nOccupy = 1;		// 设置为占用
		int nGet = pep->m_nMiddleID && !theApp.m_bNoProduct // 有中间点，不是直接取取货，到中间点
			? pep->m_nMiddleID - 1		// 如果有中间点，导航到中间点，减1变成0开始的道路表中的序号
			: pep->getRoadNode()->m_nID - 1;	// 否则导航到终点，减1变成0开始的道路表中的序号
		//int nGet = pep->getRoadNode()->m_nID - 1;	// 否则导航到终点，减1变成0开始的道路表中的序号
		Car* pc = 0;
		if(!pCar->m_nCarOccupy && pCar->isInit())
			pc = pCar;
		else
			pc = getNearCar(nGet);	// 减1变成0开始的道路表中的序号
		if (pc && !pc->m_nCarOccupy)
		{
			pc->m_nCarOccupy = 1;
			if (pc->waitGet()) // 选择的AGV正在等待，有可能正在行驶，等行驶完成以后再启动
			{
				/*
				// ?? 临时条件(pep->getType1() != pep->getType2())，同类时不到中点，直接到端点
				ASSERT(pCar->m_arrPath.GetCount() == 2);	// 端点等待，路径有两个节点
				int nn = pCar->m_arrPath[1].m_nPointID - 1;
				ASSERT(nn > 0);
				if(m_arrEndPoint[nn]->getType1() == pep->getType1())
					nGet = pep->getRoadNode()->m_nID - 1;	// 如果是同类，直接导到终点
				* /
				
				if (carStartGet(pc, nGet))
				{
					// 设置AGV对应的呼叫点通讯实例
					pCar->m_pCallSocket = pep->m_pSocket;
				}

				break;
			}
			else
				break;	// 20190330 这种情况下，最近的车还在存货，继续等待，存货后去取货
		}
		else
			break;	// 没有空闲AGV，结束
	}
	//mutexPaoGuang.Unlock();		// 线程互斥，计算去抛光线AGV时，只有一个线程工作。	//?? 未考虑锁定期间异常跳出，其它锁定线程也有这个问题


	// 端点变量清0，//?? 异常跳出后，这些变量没有清零
	for (INT_PTR i = m_arrEndPoint.GetCount() - 1; i >= 0; i--)
		m_arrEndPoint[i]->m_nOccupy = 0;
	// AGV变量清0
	for (INT_PTR i = m_arrCars.GetCount() - 1; i >= 0; i--)
		m_arrCars[i]->m_nCarOccupy = 0;
}
*/

// 查找取货工位，条件是空闲，有货优先，等待时间长优先。
// 返回0，没有空闲产品点，否则为产品点指针
/*EndPoint * Ground::getProduct()
{
	INT_PTR nLenCars = m_arrCars.GetCount();
	ASSERT(nLenCars > 0);

	BOOL bHasProduct = FALSE;	// 总体有货

	// 等待时刻，先设一个较大的数，越小时间越长
	COleDateTime dtWait(9999, 1, 1, 0, 0, 0);

	// 返回点
	EndPoint * pResult = 0;
	
	// 查找所有中间等待点并检查是否空闲，如果空闲查找最合适的AGV取货
	INT_PTR nLenPoint = m_arrEndPoint.GetCount();
	for (int i = 0; i < nLenPoint; i++)
	{
		EndPoint * pep = m_arrEndPoint[i];
		if ((pep->m_nType != 3 || !pep->m_nMiddleID)	// 不是取货点或没有中间点，跳过	// ?? 必须要有中间点
			|| pep->m_bDisabled	// 停用，跳过
			)
			continue;

		// 查找空闲中间点，条件是没有正在取货的AGV
		BOOL bPointBusy = FALSE;	// 取货点占用标识
		COleDateTime dt;	// 线等待时间
		BOOL bhp = FALSE;	// 线有货
		BOOL b2 = FALSE;	// 线占用
		for (; i < nLenPoint; i++)
		{
			EndPoint * pep2 = m_arrEndPoint[i];
			if (pep2->m_nMiddleID != pep->m_nMiddleID)
				break;	// 中间点不同，结束

			if (b2)
				continue;	// 线占用，把同一条线循环完毕
			else if (pep2->m_nOccupy)
			{
				b2 = TRUE;	// 线被占用
				continue;
			}

			// 中间点卡号相同时，算同一条线，只能去一个AGV
			// ?? 同一条线不同终点的中间点在数据库中连续排列
			int nMiddleCard = m_arrRoadNodes[pep->m_nMiddleID - 1].m_nCard;
			for (int i3 = 0; !bPointBusy && i3 < nLenCars; i3++)
			{
				Car * pc = m_arrCars[i3];
				if (pc->getDisable())
					continue;

				INT_PTR nLenPath = pc->m_arrPath.GetCount() - 1;
				// ASSERT(nLenPath > 0);
				if (nLenPath < 0)
					continue;	// 启动或初始化时没有设置路径。

				int nCard = pc->m_arrPath[nLenPath].m_nCard;
				if (nCard == pep2->getRoadNode()->m_nCard	// 端点被占用
					|| nCard == nMiddleCard	// 中点被占用
					)
				{
					bPointBusy = TRUE;		// 中点被占用，不空闲
					break;
				}
			}

			if (bPointBusy)
				continue;	// 把同一条线循环完毕

			RoadNode * prn = pep2->getRoadNode();
			pep2->getLength(prn->m_nDirect);	// 借用这个函数计算正在工作的垛，?? 重启后如果有新垛可能计算不准确
											// 20170409 同时计算取货点的取色号（产品编号）
			if (!bhp)
				bhp = (((ProductPoint*)pep2)->m_nPile != 0);

			if (bHasProduct && !bhp)
				continue;	// 如果上一次有货，本次无货，跳过

			// 记忆等待时间
			if (dt < pep2->m_dtWait)
				dt = pep2->m_dtWait;
		}

		if (bPointBusy || b2)
			continue;	// 线被占用，跳过

		// 设置取货点
		if ((!bHasProduct && bhp)		// 第一次有货，设为预取货点
			|| (bHasProduct == bhp && dt < dtWait)
			)
		{
			pResult = pep;	// 数据库索引号变数组序号
			dtWait = dt;
		}

		// 记忆有货状态
		if (!bHasProduct && bhp)
		{
			bHasProduct = TRUE;
		}
	}
	
	return pResult;
}
*/

// 查找取货工位，条件是空闲，有货优先，等待时间长优先。
// 返回0，没有空闲产品点，否则为产品点指针
// 20180129 按东鹏设计
// 20200327 没有执行到这儿，可能是清远和东鹏倒货专用
EndPoint * Ground::getProduct()
{
	INT_PTR nLenCars = m_arrCars.GetCount();
	ASSERT(nLenCars > 0);

	//BOOL bHasProduct = FALSE;	// 总体有货

	// 等待时刻，先设一个较大的数，越小时间越长
	//COleDateTime dtWait(9999, 1, 1, 0, 0, 0);

	// 返回点
	EndPoint * pResult = 0;

	// 查找所有中间等待点并检查是否空闲，如果空闲查找最合适的AGV取货
	INT_PTR nLenPoint = m_arrEndPoint.GetCount();
	for (int i = 0; i < nLenPoint; i++)
	{
		EndPoint * pep = m_arrEndPoint[i];
		if (pep->m_strNextCode.IsEmpty()	// 没有下级点
			|| pep->m_bDisabled	// 停用，跳过
			|| (pep->isStore() && pep->isEmpty()) // 库区但是没有货
			)
			continue;

		return pep;

		/* 20181010 清远家美不用

		// 查找空闲中间点，条件是没有正在取货的AGV
		BOOL bPointBusy = FALSE;	// 取货点占用标识
		COleDateTime dt;	// 线等待时间
		BOOL bhp = FALSE;	// 线有货
		BOOL b2 = FALSE;	// 线占用
		for (; i < nLenPoint; i++)
		{
			EndPoint * pep2 = m_arrEndPoint[i];
			if (pep2->m_nMiddleID != pep->m_nMiddleID)
				break;	// 中间点不同，结束

			if (b2)
				continue;	// 线占用，把同一条线循环完毕
			else if (pep2->m_nOccupy)
			{
				b2 = TRUE;	// 线被占用
				continue;
			}

			// 中间点卡号相同时，算同一条线，只能去一个AGV
			// ?? 同一条线不同终点的中间点在数据库中连续排列
			int nMiddleCard = m_arrRoadNodes[pep->m_nMiddleID - 1].m_nCard;
			for (int i3 = 0; !bPointBusy && i3 < nLenCars; i3++)
			{
				Car * pc = m_arrCars[i3];
				if (pc->getDisable())
					continue;

				INT_PTR nLenPath = pc->m_arrPath.GetCount() - 1;
				// ASSERT(nLenPath > 0);
				if (nLenPath < 0)
					continue;	// 启动或初始化时没有设置路径。

				int nCard = pc->m_arrPath[nLenPath].m_nCard;
				if (nCard == pep2->getRoadNode()->m_nCard	// 端点被占用
					|| nCard == nMiddleCard	// 中点被占用
					)
				{
					bPointBusy = TRUE;		// 中点被占用，不空闲
					break;
				}
			}

			if (bPointBusy)
				continue;	// 把同一条线循环完毕

							//RoadNode * prn = pep2->getRoadNode();
			pep2->getLength(FALSE);	// 借用这个函数计算正在工作的垛，?? 重启后如果有新垛可能计算不准确
									// 20170409 同时计算取货点的取色号（产品编号）
			if (!bhp)
				bhp = (((ProductPoint*)pep2)->m_nPile != 0);

			if (bHasProduct && !bhp)
				continue;	// 如果上一次有货，本次无货，跳过

			// 记忆等待时间
			if (dt < pep2->m_dtWait)
				dt = pep2->m_dtWait;
		}

		if (bPointBusy || b2)
			continue;	// 线被占用，跳过

						// 设置取货点
		if ((!bHasProduct && bhp)		// 第一次有货，设为预取货点
			|| (bHasProduct == bhp && dt < dtWait)
			)
		{
			pResult = pep;	// 数据库索引号变数组序号
			dtWait = dt;
		}

		// 记忆有货状态
		if (!bHasProduct && bhp)
		{
			bHasProduct = TRUE;
		}
		*/
	}

	return pResult;
}

// 查找取货工位，条件是空闲，有货优先，等待时间长优先。
// 返回0，没有空闲产品点，否则为产品点指针
// 20180129 按东鹏设计
// 20180130 加参数，nType 搜索类型
/*EndPoint* Ground::getProduct(Car* pCar, POINT_TYPE nType)
{
	ASSERT(m_arrCars.GetCount() > 0);

	int nNodeID = 0;
	if (pCar)
	{
		HLock hl(&pCar->m_mutexPath, 1, (DWORD)-32);	// 等待读，退出时自动解锁

		INT_PTR nn = pCar->m_arrPath.GetCount();
		if (nn)
		{
			ASSERT(nn == 2);	// 在端点等待
			nNodeID = pCar->m_arrPath[1].m_nID;
		}
	}

	BOOL bHasProduct = FALSE;	// 总体有货

	// 等待时刻，先设一个较大的数，越小时间越长
	COleDateTime dt(9999, 1, 1, 0, 0, 0);

	// 查找所有中间等待点并检查是否空闲，如果空闲查找最合适的AGV取货
	EndPoint * pResult = 0;
	INT_PTR nLenPoint = m_arrEndPoint.GetCount();
	for (int i = 0; i < nLenPoint; i++)
	{
		EndPoint * pep = m_arrEndPoint[i];

		if (pep->m_strNextCode.IsEmpty()	// 没有下级点
			|| pep->m_bDisabled	// 停用，跳过
			|| pep->m_pointType != nType	// 类型不相同
			|| (theApp.m_bPlcCtrl && nType == POINT_TYPE::POINT_STORE && !pep->getNextPoint2())	// 清远家美专用, 取二次包装的货，PLC控制有效
			//|| pep->getCode2() != *(PINT)L"00"	// 不是00，不导航,//?? 东鹏专用，抛光机的空托出口，送货同时出空托，不用其它AGV
			|| (pep->isEmpty() && !pep->m_nMiddleID && !theApp.m_bNoProduct)	// 如果有中间点，空的也去，20191119 m_bNoProduct为真时空的也去
			|| pep->getRoadNode()->m_nID == nNodeID	// 本AGV所在端点
			)
		{
			continue; 
		}

		// 重量匹配
		if (nType == POINT_TYPE::POINT_PRODUCT)
		{
			if (!pep->m_pTrayRecord)
				continue;	// 还没有初始化，跳过
			if (pCar && pep->m_pTrayRecord->nWeight != pCar->m_nLoad)	// 托重不等于载重
				continue;	// 重量不匹配，跳过
		}

		ASSERT(pCar);
		int nStart = pCar->m_pReadData->nStartIndex;
		TRY
		{
			BOOL b = (pCar->m_pReadData->nState == 0x1025);	// 发车到私有点
			if (!m_colls.isStartFriend(nStart, pep->getPointID() - 1, b))
				continue;	// 路径断开，不能到达，跳过。
		}
		CATCH(GException, pge)
		{
			// 保存日志
			pge->m_strObject = pCar->getIPPort();
			theApp.m_pLogList->addLog(pge);

			continue;	// 路径断开，不能到达，跳过。
		}
		END_CATCH

		if (pointBeUsed(pep))	// 已经占用，跳过
			continue;

		// 如果是同类，先取较多的	// ?? 完善同类的定义，也可能先进先出
		//if (pResult && (pResult->totlePile() > pep->totlePile()))
		//	continue;

		// 当前有货呼叫
		BOOL bhp = (pep->m_nXCurrent != 0);

		// 记忆等待时间
		// 设置取货点
		COleDateTime dt2 = getWaitTime(pep);
		if ((!bHasProduct && bhp)		// 第一次有货，设为预取货点
			|| (bHasProduct == bhp && dt > dt2)		// 要用<=，因为初始相等时就没有记录
			)
		{
			dt = dt2;
			pResult = pep;
		}

		// 记忆有货状态
		if (!bHasProduct && bhp)
			bHasProduct = TRUE;
	}

	return pResult;
}
*/
// 东鹏专用
		/* 20181010 清远家美不用
EndPoint * Ground::getProduct(Car * pCar, int nType)
{

		WCHAR cp = pep->getCode()[0];
		BOOL b窑下砖	= cp == '0';
		BOOL b库区砖	= cp == '1';
		BOOL b库区空托	= cp == '2';
		ASSERT(b窑下砖 || b库区砖 || b库区空托);
		BOOL b取砖 = b窑下砖 | b库区砖;

		if (b取砖)
		{
			/* // 最多有四个存砖
			int n78Count = 0;
			for (size_t j = 0, length = m_arrCars.GetCount(); j < length; j++)
			{
				Car * pc = m_arrCars[j];
				if (pc->getDisable())
					continue;
				if (!pc->m_pEndPoint)
					continue;

				int nCode = pc->m_pEndPoint->getCode1();
				if (nCode == *(PINT)L"37"
					|| nCode == *(PINT)L"38"
					|| pep->getCode1() == *(PINT)L"07"
					|| pep->getCode1() == *(PINT)L"08"
					|| pep->getCode1() == *(PINT)L"17"
					|| pep->getCode1() == *(PINT)L"18"
					)
					n78Count++;
			}

			if (n78Count <= 4)
			{
				// 不能连续取同一品种，不含空托
				static WCHAR cPreProduct = 0;
				if (cPreProduct != pep->getCode()[1])		// b窑下砖 || b库区砖
				{
					cPreProduct = pep->getCode()[1];
					return pep;
				}
			}* /

			// 每条抛光线最多有2个AGV存砖
			int nCount = 0;
			for (size_t j = 0, length = m_arrCars.GetCount(); j < length; j++)
			{
				Car * pc = m_arrCars[j];
				if (pc->getDisable())
					continue;	// 停用，跳过
				if (!pc->m_pEndPoint)
					continue;	// 不是终点，跳过
				if (pc->m_pEndPoint->getCode()[0] != '3')
					continue;	// 不是到抛光机，跳过

				if(pc->m_pEndPoint->getCode()[1] == pep->getCode()[1])
					nCount++;	// 线号相同，计数加1
			}

			if (nCount < 2)	// 每条抛光线最多2个AGV
			{
				// 不能连续取同一品种，不含空托
				static WCHAR cPreProduct = 0;
				if (cPreProduct != pep->getCode()[1])		// b窑下砖 || b库区砖
				{
					//cPreProduct = pep->getCode()[1];
					//return pep;
				}
				else
					continue;
			}
			else
				continue;

			// 间隔2分钟后执行一次
			INT_PTR n = m_arrEndPoint.GetCount();
			for (int j = 0; j < n; j++)
			{
				EndPoint * pep2 = m_arrEndPoint[j];
				if ( pep2->getCode()[0] == L'3'	// ?? 东鹏专用，抛光线
					&& pep->getCode()[1] == pep2->getCode()[1]	// 线号相同
					&& pep2->getCode2() == *(PINT)L"00"	// 不是00，不导航,//?? 东鹏专用，抛光机的空托出口，送货同时出空托，不用其它AGV
					)
				{
					COleDateTimeSpan ts = COleDateTime::GetCurrentTime() - pep2->m_dtWait;
					if (ts.GetTotalSeconds() > 120	// 2分钟取一次
						|| (theApp.m_bDemo && ts.GetTotalSeconds() > 20)	// 模拟时12秒，快10倍
						)
					{
						pep2->m_dtWait = COleDateTime::GetCurrentTime();
						return pep;
					}
				}
			}
		}

		// 下砖区没有空的工位，不取空托
		if (b库区空托)
		{
			for (size_t j = 0, length = m_arrEndPoint.GetCount(); j < length; j++)
			{
				EndPoint * pep2 = m_arrEndPoint[j];
				if (!pep2->isParent(pep))
					continue;
				if (pep2->isFull())
					continue;
				if (pep2->getRoadNode()->m_pCar)
					continue;
	
				return pep;	// 如果有空位，取空托
			}
		}

	}

	return 0;
}

		*/

// 优先到相邻工位取货
// ??东鹏定制
BOOL Ground::pickNeighbor(Car * pCar)
{
	HLock hl(&pCar->m_mutexPath, -1, (DWORD)-33);	// 等待写，退出时自动解锁

	INT_PTR len = pCar->m_arrPath.GetCount();
	ASSERT(len == 2);
	RoadNode * prn = &pCar->m_arrPath[1];	// 等待共两点，0开绐的第2点是终点
	ASSERT(prn->m_nPointID);
	EndPoint * pPoint = m_arrEndPoint[(INT_PTR)prn->m_nPointID-1];
	if (pPoint->m_bufCode[0] != L'3')	// 抛光线工作
		return 0;

	WCHAR buf[5];	// 四个字符加一个0
	ASSERT(sizeof(buf) == sizeof(pPoint->m_bufCode));

	memcpy(buf, pPoint->m_bufCode, sizeof(buf));
	buf[3] = L'1';
	INT_PTR nLenPoint = m_arrEndPoint.GetCount();
	for (int i = 0; i < nLenPoint; i++)
	{
		EndPoint * pep = m_arrEndPoint[i];
		if (*(PINT64)buf == *(PINT64)pep->m_bufCode		// 是相邻的工位
			&& !pep->isEmpty()		// 有货
			)
		{
			int nGet = pep->getRoadNode()->m_nID - 1;	// 否则导航到终点，减1变成0开始的道路表中的序号
			carStartGet(pCar, nGet);
			return TRUE;
		}
	}

	return 0;
}

// 清除背景，重画
void Ground::clearBackground()
{
	//mutexThread.Lock();	// 线程互斥，有可能多个线程同时要求重画背景，造成多次删除
	if (m_pBrushGround)
	{
		// ?? 资源好像没有释放，vc2019释放了
		ASSERT(m_pBrushGround->GetBitmap());
		//m_pBrushGround->GetBitmap()->Destroy();
		//m_pBrushGround->Destroy();
		delete m_pBrushGround->GetBitmap();
		delete m_pBrushGround;
		m_pBrushGround = 0;
	}
	//mutexThread.Unlock();
}

/// <类型>Fanction</类型>
/// <作者>Mr. Hu</作者>
/// <日期>20181007</日期>
/// <名称>pointBeUsed</名称>
/// <参数 name = "pPoint">指定点</参数>
/// <返回>TRUE 占用，FALSE 未占用</返回>
/// <说明>指定的端点是否已经被其它AGV占用</说明>
BOOL Ground::pointBeUsed(EndPoint * pPoint)
{
	// 遍历AGV
	INT_PTR len = m_arrCars.GetCount();
	for (INT_PTR i = 0; i < len; i++)
	{
		Car * pc = m_arrCars[i];

		if (pc->getDisable())
			continue;	// 停用和初始化，跳过

		HLock hl(&pc->m_mutexPath, 1, (DWORD)-38);	// 等待读，退出时自动解锁
		//if (!hl.allowed())			// 路径被别处锁定时跳过	// ?? 不能跳过，会产生误动作，端点本来被占了，而返回可用
		//	continue;
			
		INT_PTR ll = pc->m_arrPath.GetCount();

		if (ll == 0)
			continue;	// 没有路径，跳过
		
		if (ll < 3		// 端点等待
			&& !theApp.m_bPlcCtrl)	// 在端点待待，有呼叫控制
		{
			continue;	//  20181207 在端点等待，无呼叫控制如是光华演示路线，否则会出现两台AGV互相等
		}

		// 正在存取货，占用
		RoadNode* ppn = pPoint->getRoadNode();
		if (pc->m_arrPath[ll - 1].m_nID == ppn->m_nID)
			return TRUE;

		// 终点PLC IP和PLC地址一样，算同一点
		if (pc->m_pEndPoint
			//&& pPoint->m_pSocket
			//&& pPoint->m_pSocket == pc->m_pEndPoint->m_pSocket
			&& ppn->m_nCard == pc->m_pEndPoint->getRoadNode()->m_nCard
			)
			return TRUE;

		// 存取货后没出第一段，占用
		if (pc->m_nRunIndex == 1 && pc->m_arrPath[0].m_nCard == ppn->m_nCard)	// 在起始路段算占用，其它AGV不能取货
			return TRUE;

		// 没有中间点，pass
		if (!pPoint->m_nMiddleID)
			continue;

		// 取货后等待存货
		if (ll == 2 && pc->m_arrPath[1].m_nPointID		// 取货后等待存货
			&& m_arrEndPoint[(INT_PTR)pc->m_arrPath[1].m_nPointID - 1]->m_nMiddleCard == pPoint->m_nMiddleCard)	// 与pc有相同的中点
		{
			return TRUE;
		}

		// 存取货后没出第一段，占用
		if (pc->m_nRunIndex == 1 && pc->m_arrPath[0].m_nPointID /*&& !pc->isTurning()*/		// 在起始路段算占用，并且第一点是端点
			&& m_arrEndPoint[(INT_PTR)pc->m_arrPath[0].m_nPointID - 1]->m_nMiddleCard == pPoint->m_nMiddleCard)	// 与pc有相同的中点
		{
			return TRUE;
		}

		// 中间点被占用
		if (pc->m_pEndPoint && pc->m_pEndPoint->m_nMiddleCard == pPoint->m_nMiddleCard)
			return TRUE;

		// 中间点被占用
		if (!pc->m_pEndPoint)
		{
			PathNode& pn = pc->m_arrPath[ll - 1];
			if (pn.m_nCard == pPoint->m_nMiddleCard)
				return TRUE;
		}
	}

	return FALSE;
}

/// <说明>启动软件时从数据库读取产品信息，如果数据表中没有信息，新建一个
///		换色号时改变产品信息
/// </说明>
void Ground::setProductInfo(ProductPoint * pEndPoint)
{
	INT_PTR n = m_arrEndPoint.GetCount();
	for (int j = 0; j < n; j++)
	{
		EndPoint * pep2 = m_arrEndPoint[j];
		if (pEndPoint->isNeighbor2(pep2))	// 生产线号相同
		{
			ASSERT(pep2->IsKindOf(RUNTIME_CLASS(ProductPoint)));

			ProductPoint * ppp = ((ProductPoint *)pep2);

			ppp->m_strProductModel = pEndPoint->m_strProductModel;
			ppp->m_strProductNumber = pEndPoint->m_strProductNumber;
			ppp->m_strProductGrade = pEndPoint->m_strProductGrade;
		}
	}
}

/// <说明>设置托号，修改point表</说明>
// ?? 在呼叫线程中执行，效率低
void Ground::changeTray(PointSocket * pPointSocket)
{
	// 更新同类托号
	INT_PTR n = m_arrEndPoint.GetCount();
	for (int i = 0; i < n; i++)
	{
		111111111;
		EndPoint * pep1 = m_arrEndPoint[i];
		//if (pep1->m_pSocket != pPointSocket)
		//	continue;

		Tray::Record* precord = theApp.m_tray.getRecord(pPointSocket->m_nTray, pep1->m_bufCode);
		if (!precord)
		{
			// 保存日志
			GException ge;
			ge.m_nCode = 1000;
			ge.m_strObject = L"Ground::changeTray()";
			ge.m_strSummary.Format(L"托号(%i)不匹配，修改后重新运行。", pPointSocket->m_nTray);
			//ge.m_strDetails = L"避让算法异常，Ground::onInterfere";

			theApp.m_pLogList->addLog(&ge);

			AfxMessageBox(ge.m_strSummary, MB_ICONSTOP);
			return;
		}

		pep1->changeTray(precord->id);
	}
	// 更新产品表的托号
	CString strsql;
	strsql.Format(L"UPDATE `products` set `model` = '%i' WHERE `pointID` IN "
		L"(SELECT `id` FROM `point` WHERE `PLCIP` = '%s');"	// IP相同为一类
		, pPointSocket->m_nTray, (LPCWSTR)pPointSocket->m_strIP
	);
	theApp.getDatabase()->Execute(strsql);

	//pep->updateProductInfo();	// 保存托号到products表中
	//setProductInfo(pep);	// 更新同类点
}

/// <说明>如果当前点相同，并且下一点也相同，表示同向同路</说明>
BOOL Ground::isSameDirection(CArray<PathNode, PathNode&>& arrPath1, INT_PTR nIndex1, CArray<PathNode, PathNode&>& arrPath2, INT_PTR nIndex2)
{
	ASSERT(nIndex1 >= 0);
	ASSERT(nIndex2 >= 0);
	//ASSERT(arrPath1.GetCount() <= nIndex1 - 1);
	//ASSERT(arrPath2.GetCount() <= nIndex2 - 1);

	if (arrPath1.GetCount() > nIndex1 - 1
		|| arrPath2.GetCount() > nIndex2 - 1)
	{
		return FALSE;
	}

	return arrPath1[nIndex1].m_nCard == arrPath2[nIndex2].m_nCard
		&& arrPath1[nIndex1 + 1].m_nCard == arrPath2[nIndex2 + 1].m_nCard
		;
}

/// <说明>设置活动行，如果端点对应多个取货点，弹出对话框选择</说明>
/*void Ground::setStoreActive(StorePoint* pep)
{
	CArray<PointSocket *> arr;
	INT_PTR len1 = m_arrEndPoint.GetCount();
	INT_PTR len2 = 0;
	for (INT_PTR i = 0; i < len1; i++)
	{
		EndPoint* pep2 = m_arrEndPoint[i];
		if (pep2->getNextPoint() == pep)
			return;	// 已经激活，返回
		if (pep2->getNextPoint2() == pep)
			return;	// 二次倒货激活，返回

		if (pep2->m_bDisabled)
			continue;
		if (!pep->isParent(pep2))
			continue;	// 不是父类，跳过
		PointSocket* pSocket = pep2->m_pSocket;
		if (!pSocket)
			continue;	// 不是呼叫点，跳过		// ?? 未考虑二次倒货 

		len2 = arr.GetCount();
		INT_PTR j = 0;
		for (; j < len2 && pSocket != arr[j]; j++);
		if (j == len2)	// 不在数组中
			arr.Add(pSocket);
	}

	len2 = arr.GetCount();
	ASSERT(len2);	// ?? 异常

	int nn = 0;
	if (len2 > 1)	// 只有一项，直接激活
	{
		// 多项，打开对话框，选择
		DlgCallPoint dlg(&arr);
		if (dlg.DoModal() != IDOK)
			return;

		nn = dlg.m_nSelCur;
		if (nn < 0)
			return;	// 没有选择，返回
	}

	ASSERT(nn < arr.GetCount());
	arr[nn]->setStoreActive(pep->m_nSerial);
	setStoreActive(arr[nn]);
}
*/

/// <作者>Mr. Hu</作者>
/// <日期>20200507</日期>
/// <功能>充电</功能>
/// <参数 name = "pCar">指定AGV</参数>
/// <返回>TRUE成功，FALSE没找到取货点</返回>
BOOL Ground::carRecharge(Car* pCar)
{
	//if (!pCar->isLowBattery())
	//	return 0;

	// 已经在充电，排除
	if (pCar->m_pEndPoint && pCar->m_pEndPoint->isRecharge())
		return FALSE;

	// 起始点
	int nStart;
	{
		HLock hl(&pCar->m_mutexPath, 1, (DWORD)-34);	// 等待读，退出时自动解锁
		//ASSERT(pCar->m_arrPath.GetCount() == 2);	// 在端点等待
		size_t len = pCar->m_arrPath.GetCount();
		ASSERT(len >= 2);	// 20201102 改在>=2，因为在线充电完成后路径多
		nStart = pCar->m_arrPath[len - 1].m_nID - 1;
	}

	// 充电点集合，这个集合要求相同的线号，不能用
	//Collection* recharges = m_colls.getRecharges();
	//CArray< EndPoint* >& arr = recharges->m_arrPoint;		// 同类型端点集
	//INT_PTR nLenPoint = arr.GetCount();

	INT_PTR nLenPoint = m_arrEndPoint.GetCount();
	for (INT_PTR i = 0; i < nLenPoint; i++)
	{
		EndPoint* pep = m_arrEndPoint[i];

		if (!pep->isRecharge())
			continue;

		if (2 != pep->allowPickup( pCar )		// 停用，跳过
			)
		{
			continue;
		}

		int nGet = pep->getRoadNode()->m_nID - 1;	// 否则导航到终点，减1变成0开始的道路表中的序号
		ASSERT(nGet >= 0);
		TRY
		{
			//m_pAStar->getPath(nStart, nGet);
			//pCar->setPath(m_pAStar->getPath());
			AStar aStar(m_arrRoadNodes);
			aStar.getPath(nStart, nGet);

			// 20210323 如果是继续向前行驶，把原路径的到数第二点加到新路径的第一点
			// 20220219 以下适用于从道路中间改道，只有发车用
			INT_PTR len1 = pCar->m_arrPath.GetCount();
			if (len1 > 1)
			{
				ASSERT(aStar.m_arrPath.GetCount() > 1);
				// 如果不是回头路，把上一次路径的倒数第二个点加新路径的第一点。
				if (
					//(pCar->m_pReadData->nState == 0x1025)								// 20220219 只有发车用
					(len1 == 2)								// 20220219 只有发车用
					&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[1].m_nCard)
					&& (pCar->m_arrPath[0].m_nCard != aStar.m_arrPath[0].m_nCard)		// 排除发车时反向行行驶
					)
				{
					// ?? 没有计算m_G和m_F
					aStar.m_arrPath.InsertAt(0, pCar->m_arrPath[0]);
				}
			}

			pCar->setPath(aStar.getPath(), nGet);		// 可能改变pCar->m_arrPath.m_nSize;
		}
		CATCH(GException, pge)
		{
			// 保存日志
			pge->m_strObject = pCar->getIPPort();
			theApp.m_pLogList->addLog(pge);

			continue;	// 路径断开，不能到达，跳过。
		}
		END_CATCH

		// 开始行走
		// 潜伏式从中间存取货，开始运行再清初始化标志
		//if (pCar->m_pReadData->sd.sd9.nCmd & 0x1000	// AGV初始化
		//	)
		//{
		//	// 复位初始化信号
		//	pCar->m_pReadData->sd.sd9.nCmd &= ~0x1000;
		//	//pCar->m_pSocket->sendData1(pCar->m_pReadData->sd.sd9.nCmd);
		//}

		// 开始行走
		//RoadNode& rn = m_arrRoadNodes[nGet];
		//ASSERT(rn.m_nPointID > 0);
		//pCar->setEndNode(&rn, m_arrEndPoint[(INT_PTR)rn.m_nPointID - 1]);

		//pCar->m_bCanRecharge = FALSE;

		return TRUE;
	}

	return FALSE;
}

/// <说明>装入起始点，其中用到m_nAStar</说明>
//void Ground::loadStartPoint()
//{
//	CString strsql =
//		L"SELECT `id`, `serial`, `road`, `points` FROM `startpoint`;";
//	HRecordset rs(theApp.getDatabase());
//	rs.Open2(strsql);
//
//	int nSerial;
//	CString strPoints;
//	CDBVariant var;	// var 要释放
//	for (int i = 0; !rs.IsEOF(); i++, rs.MoveNext())	// ?? 超出cn1处理
//	{
//		// 读数据
//		// 起点序号
//		rs.GetFieldValue((short)1, var);
//		nSerial = var.m_lVal;	// ?? 异常
//
//		// 起始点
//		rs.GetFieldValue((short)2, var);
//		StartPoint* psp = new StartPoint;
//		psp->m_nSerial = nSerial;
//		psp->m_nRoad = var.m_lVal;
//
//		// 对应Point表的终点集m_arrPoints1
//		rs.GetFieldValue((short)3, strPoints);
//
//		// 如果有终点集，直接设置终点表
//		if (!strPoints.IsEmpty())
//		{
//			std::wregex rx(L"(\\d+),?");	// 如果是unicode码时，用wregex和wstring;
//			std::wcmatch mr;			// 正则匹配结果
//			PCWCHAR pb = LPCWSTR(strPoints);
//			for (size_t j = 0; regex_search(pb, mr, rx); )	// ?? 写成这种形式regex_match(std::wstring(str), mr, rx)，运行时出错。
//			{
//				pb = mr[0].second;
//
//				// 在point表中的序号，从0开始，ID-1
//				int np = _wtoi(mr[1].first);	// mr[0]是全部字符串，mr[1]是匹配的第一个字符串。
//				if (np >= m_arrEndPoint.GetCount())
//				{
//					AfxMessageBox(L"终点号太大。");
//					continue;
//				}
//
//				psp->m_arrPoints1.Add(np);
//
//				j++;	// 这里加，因为中间跳过的不算
//			}
//		}
//
//		// 用AStar计算所有能到达的终点m_arrPoints2，// ?? 每个点都用AStar，效率低，可以用之前算过的。
//		int ns = psp->m_nRoad;
//		for (size_t j = 0, n = m_arrEndPoint.GetCount(); j < n; j++)	// ?? 超出cn2处理
//		{
//			EndPoint* pep = m_arrEndPoint[j];
//			if (pep->m_strNextCode.IsEmpty() && !pep->isRecharge())
//				continue;	// 只查取货点，非取货点跳过，20190816 充电点不跳过
//
//			TRY
//			{
//				int ne = pep->getRoadNode()->m_nID - 1;
//				if (ns != ne)	// 不相等时验证，相等时直接加入。// ?? 如果发车后导航到本段，异常
//				{
//					AStar aStar(m_arrRoadNodes);
//					aStar.getPath(ns, ne);	// 计算两点间的路径	// ?? 用异常跳过下面的程序，不合理
//				}
//
//				psp->m_arrPoints2.Add((int)j);
//
//				//tc.getCounter();	// 0.1ms
//			}
//			CATCH_ALL(e)	// ?? 分情况进行异常处理
//			{
//				//e->ReportError();		// 不能写日志，不显示信息
//			}
//			END_CATCH_ALL
//		}
//
//
//		m_startPoints.add(psp);
//	}
//	var.Clear();	// var 要释放
//	rs.Close();
//
//}

/// <类型>Fanction</类型>
/// <作者>Mr. Hu</作者>
/// <日期>20200402</日期>
/// <功能>取同一条线的等待时间</功能>
/// <参数 name = "pep">指定的取货点</参数>
/// <返回>最近的时间</返回>
/// <说明>取同一条线各点的最近一次取货时间</说明>
COleDateTime Ground::getWaitTime(EndPoint* pep)
{
	COleDateTime dt = pep->m_dtWait;
	if (theApp.m_bNoProduct)
		return dt;	// 没有货也去取货时，不判断同一条线

	int code = pep->getCode1();
	for (size_t i = 0, length = m_arrEndPoint.GetCount(); i < length; i++)
	{
		EndPoint* pep2 = m_arrEndPoint[i];
		if (!pep2->m_bExtCtrl)
			continue;	// 不是产品点跳过

		if (code != pep2->getCode1())	
			continue;	// 不是同一条线跳过

		if (dt < pep2->m_dtWait)
			dt = pep2->m_dtWait;
	}
	return dt;
}

/// <作者>Mr. Hu</作者>
/// <日期>202401004</日期>
/// <功能>处理接收数据</功能>
/// <参数 name = "pRecBuf">接收数据缓冲</参数>
/// <参数 name = "len">接收数据长度</参数>
/// <返回></返回>
/// <说明> 回调函数具体实现 https://blog.csdn.net/fairytailq/article/details/78376414#:~:text=MFC%E4%B8%AD%E5%BA%94%E8%AF%A5%E6%9C%89%E4%B8%A4%E7%B1%BB%E5%9B%9E
///			线程中运行
///</说明>
void Ground::wcsRead(PBYTE pRecBuf, int len)
{
	int nTaskLen = 0;
	switch (*(UINT32*)pRecBuf)
	{
	case 0:									// AGV心跳数据
	{
		nTaskLen = sizeof(CarData);			// 数据长度不对
		if (len != nTaskLen)
			break;

		CarData& carData = (CarData&)*pRecBuf;
		UINT32 nCar = carData.nCar;
		ASSERT(nCar > 0 && nCar < 256);		// 车号超范围
		if (nCar == 0 || nCar >= 256)
			break;

		Car* pCar = m_carList[nCar];
		//ASSERT(pCar);
		if (pCar)
		{
			pCar->readData(carData);
		}

		break;
	}

	case 1:									// 任务数据
	{
		WcsSatae wcsSatae;
		WcsTask& wcsTask = (WcsTask&)*pRecBuf;
		Car* pCar = 0;

		nTaskLen = sizeof(WcsTask);
		if (len != nTaskLen)
			wcsSatae.bErrLength = 1;
		else
		{
			CString strCode((PCHAR)wcsTask.nDest);
			CString strNumber((PCHAR)wcsTask.sNumber);
			{
				CString strCode2(strCode);
				CString strNumber2(strNumber);
				strCode2.Replace(L"'", L"''");
				strNumber2.Replace(L"'", L"''");

				// 存入数据库
				CString strsql;
				strsql.Format(
					L"INSERT INTO `wcstask`"
					L"(`sid`,`car`,`type`,`priority`,`tray`,"
					L"`destcode`,`serial`,`number`) "
					L"VALUES"
					L"(%i, %i, %i, %i, %i,"
					L"'%s', %i, '%s'"			// 最后一项，没有逗号
					L");",

					wcsTask.nSid,
					wcsTask.nCar,
					wcsTask.nType,
					wcsTask.nPriority,
					wcsTask.nTray,

					(LPCTSTR)strCode2,
					wcsTask.nSerial,
					(LPCTSTR)strNumber2
					//(LPCTSTR)sdt
				);
				theApp.getDatabase()->Execute(strsql);	// ?? 异常处理
			}

			// 判断SID
			if (m_wcsTask.nSid == wcsTask.nSid)
			{
				GException ge;
				ge.m_nCode = 2039;
				ge.m_strObject = L"WCS任务";
				ge.m_strSummary.Format(L"SID(%i)重复", wcsTask.nSid);
				//ge.m_strDetails.Format(L"希望长度%i，实际长度%i。", ll, len);
				theApp.m_pLogList->addLog(&ge);

				wcsSatae.bSidRepetition = 1;
			}
			else
			{
				m_wcsTask = wcsTask;
				UINT32 nCar = wcsTask.nCar;

				// 执行任务
				if (wcsTask.nType == 0 && nCar == 0)				// 取消任务，并且无车号
				{
				}
				else if (wcsTask.nType == 0)				// 取消任务，有车号
				{
					ASSERT(nCar > 0 && nCar < 256);
					if (nCar > 0 && nCar < 256 &&
						(pCar = m_carList[nCar]) != 0
						)
					{
						pCar->cancelTask(wcsTask);
						m_wcsTask.nSid = (UINT32)-1;
					}
					else
					{
						GException ge;
						ge.m_nCode = 2036;
						ge.m_strObject = L"WCS任务";
						ge.m_strSummary.Format(L"AGV编号(%i)错误", nCar);
						//ge.m_strDetails.Format(L"希望长度%i，实际长度%i。", ll, len);
						theApp.m_pLogList->addLog(&ge);

						wcsSatae.bNoAGV = 1;
					}
				}
				else										// 创建新任务
				{
					m_colls.wcsTask(pRecBuf);
					return;									// m_colls.wcsTask(pRecBuf)中已经响应任务返回路径，直接跳出
				}
			}
		}

		// 响应任务，返回异常状态
		HBuffer buf(sizeof(WcsPath));								// 申请内存，自动释放
		WcsPath& wcsPath = (WcsPath&)*buf.getBuf();					// 转换成 WcsPath 结构
		wcsPath.nCode = 1;											// 路径数据
		wcsPath.nSid = wcsTask.nSid;								// 返回sid
		wcsPath.nCar = !wcsTask.nCar ? !pCar ? 0
			: pCar->m_nFinsNode : wcsTask.nCar;						// 如果没有指定agv，并且没有空闲agv，取0，否则返回agv编号，如果指定了AGV，无论是否可用，返回指定的AGV编号
		memcpy(wcsPath.sNumber, wcsTask.sNumber,
			sizeof(wcsPath.sNumber));								// 任务编号
		wcsPath.nState = wcsSatae.nState;							// 任务状态，0正常，详见声明
		wcsPath.nLength = 0;										// 路径节点数量

		m_socketWcs.sendData((LPCSTR)buf.getBuf(), sizeof(WcsPath));			// 返回任务指令
		break;
	}

	case 2:									// 库位数据
	{
		WcsSetPile wsp = *(WcsSetPile*)pRecBuf;
		WcsReturnPile wrp = { wsp.nCode, wsp.nSid, wsp.nPointID, wsp.nX, wsp.nZ, 0 };
		nTaskLen = sizeof(WcsSetPile);
		GException ge;
		if (len != nTaskLen)
		{
			wrp.bErrLength = 1;
			m_socketWcs.sendData((LPCSTR)&wrp, sizeof(wrp));			// 返回任务指令
			break;
		}
		INT_PTR nCount = m_arrEndPoint.GetCount();
		UINT32 nSerial = wsp.nPointID - 1;
		if (nSerial >= nCount)
		{
			ge.m_nCode = 2050;
			ge.m_strSummary.Format(L"站点ID(%i)超界(%i)。", wsp.nPointID, (int)nCount);
			wrp.bErrPointID = 1;
		}
		else
		{
			EndPoint* pep = m_arrEndPoint[nSerial];
			if ((int)wsp.nX > pep->m_nXNumber)
			{
				ge.m_nCode = 2051;
				ge.m_strSummary.Format(L"垛数(%i)超界(%i)。", wsp.nX, pep->m_nXNumber);
				wrp.bErrX = 1;
			}
			else if ((int)wsp.nZ > pep->m_nZNumber)
			{
				ge.m_nCode = 2052;
				ge.m_strSummary.Format(L"层数(%i)超界(%i)。", wsp.nZ, pep->m_nZNumber);
				wrp.bErrZ = 1;
			}
			else
			{
#pragma 警告：nZ有时是0，暂时统一改为1
				if (wsp.nX > 0)
					wsp.nZ = 1;

				CString strsql;
				strsql.Format(L"UPDATE `point` SET `xCurrent` = %i, `zCurrent` = %i"
					L" WHERE `id` = %i;", wsp.nX, wsp.nZ, wsp.nPointID
				);
				theApp.getDatabase()->Execute(strsql);	// ?? 异常处理

				pep->m_nXCurrent = wsp.nX;
				pep->m_nZCurrent = wsp.nZ;
			}
		}

		if (wrp.nState)
		{
			ge.m_strObject = L"库位设置";
			theApp.m_pLogList->addLog(&ge);
		}

		m_socketWcs.sendData((LPCSTR)&wrp, sizeof(wrp));			// 返回任务指令
		break;
	}

	default:
		break;
	}

	if (len != nTaskLen)
	{
		GException ge;
		ge.m_nCode = 2030;
		ge.m_strObject.Format(L"WCS长度错误，报文(%i)", *(UINT32*)pRecBuf);
		ge.m_strSummary.Format(L"设定长度(%i)，接收数据长度(%i)。", nTaskLen, len);
		//ge.m_strDetails.Format(L"希望长度%i，实际长度%i。", ll, len);
		theApp.m_pLogList->addLog(&ge);
		return;
	}
}

/// <作者>Mr. Hu</作者>
/// <日期>202401012</日期>
/// <功能>从任务中取对应的AGV</功能>
/// <参数 name = "pRecBuf">接收数据缓冲</参数>
/// <返回></返回>
/// <说明> 回调函数具体实现 https://blog.csdn.net/fairytailq/article/details/78376414#:~:text=MFC%E4%B8%AD%E5%BA%94%E8%AF%A5%E6%9C%89%E4%B8%A4%E7%B1%BB%E5%9B%9E
///			线程中运行
///</说明>
//Car* Ground::getCarFromList(CarData& carData)
//{
//	UINT32 nCar = carData.nCar;
//	ASSERT(nCar > 0 && nCar < 256);
//	if (nCar == 0 || nCar >= 256)
//		return 0;
//
//	Car* pCar = m_carList[nCar];
//	return pCar;
//}
